From 104557b6e4b388752fa357acec89953cdcfcf606 Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Wed, 5 Aug 2015 21:17:59 -0300 Subject: [PATCH 001/106] Use the first version found in the response. In releases_cmd.go we can break early from the version check loop --- releases_cmd.go | 1 + releases_cmd_test.go | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/releases_cmd.go b/releases_cmd.go index e3ef6f4..5b02cb2 100644 --- a/releases_cmd.go +++ b/releases_cmd.go @@ -46,6 +46,7 @@ func (r *releasesCmd) printFiles(version string, for _, release := range releases { if release.Version == version { files = release.UserFiles + break } } if files == "" { diff --git a/releases_cmd_test.go b/releases_cmd_test.go index 008eb9a..f67c442 100644 --- a/releases_cmd_test.go +++ b/releases_cmd_test.go @@ -55,7 +55,7 @@ func TestReleasesCmdError(t *testing.T) { ensure.NotNil(t, c.run(h.env, &client{})) } -func TestReleasesCmdPrintVersin(t *testing.T) { +func TestReleasesCmdPrintVersion(t *testing.T) { h, r := newReleasesCmdHarness(t) releases := []releasesResponse{ {Version: "v1", @@ -69,6 +69,12 @@ func TestReleasesCmdPrintVersin(t *testing.T) { "public": {"index.html": "2", "docs.html": "2"} }`, }, + {Version: "v2", + UserFiles: `{ + "cloud": {"v2_main.js": "2", "v2_app.js": "2", "views/v2_docs.js": "2"}, + "public": {"v2_index.html": "2", "v2_docs.html": "2"} + }`, + }, } err := r.printFiles("", releases, h.env) ensure.Err(t, err, regexp.MustCompile("Unable to fetch files for release version")) From 9c706cb47d3a16d6e51be960639bcb8a5e2482ee Mon Sep 17 00:00:00 2001 From: Alberto Leal Date: Thu, 6 Aug 2015 09:41:27 -0300 Subject: [PATCH 002/106] Create an anonymous func to remove duplicate code. In android_symbols uploader getBuildVersion, we can abstract out error handling into a separate anonymous function. --- android_symbol_uploader.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/android_symbol_uploader.go b/android_symbol_uploader.go index d633581..72e6337 100644 --- a/android_symbol_uploader.go +++ b/android_symbol_uploader.go @@ -131,8 +131,7 @@ func parseVersionFromBytes(bytes []byte) (int, error) { } func (a *androidSymbolUploader) getBuildVersion(e *env) (int, error) { - if a.Manifest != "" { - versionCode, err := a.getBuildVersionFromManifest() + var handleError = func(versionCode int, err error) (int, error) { if err != nil { return 0, err } @@ -142,14 +141,11 @@ func (a *androidSymbolUploader) getBuildVersion(e *env) (int, error) { return versionCode, nil } - versionCode, err := a.getBuildVersionFromAPK() - if err != nil { - return 0, err - } - if versionCode == 1 { - fmt.Fprintln(e.Out, "Warning: build number is '1'.") + if a.Manifest != "" { + return handleError(a.getBuildVersionFromManifest()) } - return versionCode, nil + + return handleError(a.getBuildVersionFromAPK()) } func (a *androidSymbolUploader) uploadSymbols(e *env) error { From 3d9c1cea38d7e73688b4558c79a0f26390223847 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 10 Aug 2015 21:07:42 +0000 Subject: [PATCH 003/106] add travis ci config --- .travis.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b551be5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: go +go: +- 1.4.2 +script: go test +before_deploy: +- go get github.com/mitchellh/gox +- gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" + -build-toolchain +- gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" + -output="parse_{{.OS}}_{{.Arch}}" +- mv parse_darwin_amd64 parse +- mv parse_windows_386.exe parse.exe +- mv parse_linux_386 parse_linux +deploy: + provider: releases + api_key: + secure: ZnquaugjmlJ6geWvXCVjZeN2Z1NZyOaNWbY1hY8LAZZR+QqlRUR+6+QZ85vHYnqHAqKHDoh8BcZUR+2hK9UV2Lqsq0aTzvuxxVIKFiF1Gc3eC6zK3FUPgk9eOnxPZM1REGM7/i977HckpNVirskdHK1esdkKOYIfLQaHW75+xZYNJ/RKUceVrrT99IlOLWH+52vd/xGY3osV65FWkF8QS8LsZrqDQlrGyeht/P5YxepkyvMxGhBs7O3j5Npldmvdce5AQttY4MXQkMXkIrbQ2u4auBtw94hn9POd2msO0pq9JF76Is3FsH4B0/3AUrWkxMHV5XAwK1ziXniVKqQeIzaPadV92r8ihySYOQABCfQgQrHaH03jG+zE4CqqxbQvVUCCoszTZRg0AFAbaYeTKngN8kaaEEiJrKsmEASHfc8cql2TYH9Tr+ZvF/dxyYiBDU/i+BjJ4Y5jpoyXrUFeeLKRSSN4WGYYaC0r4aCBNcVDQFo9sd0n1lUtznc6wtg671Qgrvq/aog7I4qJKYBeJ/OGm771IboPxLob3P5hBdMZwOFPbjFsOmMhyBvYwxBJz3SJfVW/+pCCQdfqbfHSB0ZawdeRBQdm3w24hvq5wdpk5nGTdbdCbxb/9LsVd1WuU4xE8PiTpXxn1WqiYEcVjFd2NtYkBtFliAvnEKax+Ew= + file: + - parse + - parse.exe + - parse_linux + - parse_linux_arm + skip_cleanup: true + on: + repo: ParsePlatform/parse-cli + tags: true + condition: $TRAVIS_TAG =~ ^release_.* From bfd172ea8a7aa7e0ac2861c05d185c016ceecc38 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 10 Aug 2015 21:17:06 +0000 Subject: [PATCH 004/106] accept short-forms for new & existing in 'new' command --- new_cmd.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/new_cmd.go b/new_cmd.go index 2b7d578..0d5eb83 100644 --- a/new_cmd.go +++ b/new_cmd.go @@ -188,7 +188,7 @@ func (n *newCmd) shouldCreateNewApp(e *env) string { var decision string fmt.Fprintf(e.Out, `Would you like to create a new app, or add Cloud Code to an existing app? -Type "new" or "existing": `) +Type "(n)ew" or "(e)xisting": `) fmt.Fscanf(e.In, "%s\n", &decision) return strings.ToLower(decision) } @@ -249,24 +249,21 @@ func (n *newCmd) run(e *env) error { app *app err error ) - decision := n.shouldCreateNewApp(e) - if decision != "new" && decision != "existing" { - return errors.New("`new` and `existing` are the only valid options") - } - isNew := false - switch decision { - case "new": + switch n.shouldCreateNewApp(e) { + case "new", "n": isNew = true app, err = apps.createApp(e) if err != nil { return err } - case "existing": + case "existing", "e": app, err = addCmd.selectApp(e) if err != nil { return err } + default: + return errors.New("`new` and `existing` are the only valid options") } if err := n.setupSample(e, app, isNew); err != nil { From 7f5b3653efbef94bc4c7d91d672c118c260a9d42 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 10 Aug 2015 14:34:44 -0700 Subject: [PATCH 005/106] add build status to readme add travis-ci build status to readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index be5afd0..7cebb3b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ Parse Command Line Tool ======= +[![Build Status](https://travis-ci.org/ParsePlatform/parse-cli.svg?branch=master)](https://travis-ci.org/ParsePlatform/parse-cli) The `Parse Command Line Tool` allows you to interact with your Cloud Code from the terminal. From 50324c87804221f02a56c16d8de5b4addb7e62f2 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 10 Aug 2015 21:46:22 +0000 Subject: [PATCH 006/106] edit travis-ci script section --- .travis.yml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index b551be5..21470e3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,15 @@ language: go go: -- 1.4.2 -script: go test + - 1.4.2 +script: + - go test -v ./... before_deploy: -- go get github.com/mitchellh/gox -- gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" - -build-toolchain -- gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" - -output="parse_{{.OS}}_{{.Arch}}" -- mv parse_darwin_amd64 parse -- mv parse_windows_386.exe parse.exe -- mv parse_linux_386 parse_linux + - go get github.com/mitchellh/gox + - gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" -build-toolchain + - gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" -output="parse_{{.OS}}_{{.Arch}}" + - mv parse_darwin_amd64 parse + - mv parse_windows_386.exe parse.exe + - mv parse_linux_386 parse_linux deploy: provider: releases api_key: From 3ba44cd213bc8e80d5b37444fab76a738d018ef3 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 10 Aug 2015 22:19:31 +0000 Subject: [PATCH 007/106] rename loginCmd to login across the entire code --- apps.go | 4 ++-- apps_test.go | 4 ++-- login_cmd.go => login.go | 35 ++++++++++-------------------- login_cmd_test.go => login_test.go | 10 ++++----- 4 files changed, 21 insertions(+), 32 deletions(-) rename login_cmd.go => login.go (85%) rename login_cmd_test.go => login_test.go (96%) diff --git a/apps.go b/apps.go index 8ccdf3d..c5c9a82 100644 --- a/apps.go +++ b/apps.go @@ -34,7 +34,7 @@ type app struct { } type apps struct { - login loginCmd + login login } const numRetries = 3 @@ -91,7 +91,7 @@ func getAuthHeaders(credentials *credentials, headers http.Header) http.Header { } func fetchAppKeys(e *env, appID string) (*app, error) { - l := &loginCmd{} + l := &login{} err := l.authUser(e) if err != nil { return nil, err diff --git a/apps_test.go b/apps_test.go index c5fa6ed..21a9519 100644 --- a/apps_test.go +++ b/apps_test.go @@ -18,8 +18,8 @@ import ( var ( defaultCredentials = credentials{email: "email", password: "password"} defaultTokenCredentials = credentials{email: "email", token: "token"} - defaultApps = apps{login: loginCmd{credentials: defaultCredentials}} - defaultAppsWithToken = apps{login: loginCmd{credentials: defaultTokenCredentials}} + defaultApps = apps{login: login{credentials: defaultCredentials}} + defaultAppsWithToken = apps{login: login{credentials: defaultTokenCredentials}} ) func newTestApp(suffix string) *app { diff --git a/login_cmd.go b/login.go similarity index 85% rename from login_cmd.go rename to login.go index 97f52da..46c315a 100644 --- a/login_cmd.go +++ b/login.go @@ -12,7 +12,6 @@ import ( "github.com/facebookgo/stackerr" "github.com/mitchellh/go-homedir" "github.com/skratchdot/open-golang/open" - "github.com/spf13/cobra" ) type credentials struct { @@ -21,7 +20,7 @@ type credentials struct { token string } -type loginCmd struct { +type login struct { credentials credentials tokenReader io.Reader } @@ -35,7 +34,7 @@ to reset your password`) parseNetrc = filepath.Join(".parse", "netrc") ) -func (l *loginCmd) populateCreds(e *env) error { +func (l *login) populateCreds(e *env) error { if l.credentials.email != "" && l.credentials.password != "" { return nil } @@ -63,7 +62,7 @@ func (l *loginCmd) populateCreds(e *env) error { return nil } -func (l *loginCmd) getTokensReader() (io.Reader, error) { +func (l *login) getTokensReader() (io.Reader, error) { if l.tokenReader != nil { return l.tokenReader, nil } @@ -80,7 +79,7 @@ func (l *loginCmd) getTokensReader() (io.Reader, error) { return file, nil } -func (l *loginCmd) getTokenCredentials(e *env) (*credentials, error) { +func (l *login) getTokenCredentials(e *env) (*credentials, error) { reader, err := l.getTokensReader() if err != nil { return nil, stackerr.Wrap(err) @@ -103,7 +102,7 @@ func (l *loginCmd) getTokenCredentials(e *env) (*credentials, error) { }, nil } -func (l *loginCmd) updatedNetrcContent( +func (l *login) updatedNetrcContent( e *env, content io.Reader, credentials *credentials, @@ -132,7 +131,7 @@ func (l *loginCmd) updatedNetrcContent( return updatedContent, nil } -func (l *loginCmd) storeCredentials(e *env, credentials *credentials) error { +func (l *login) storeCredentials(e *env, credentials *credentials) error { if l.tokenReader != nil { // tests should not store credentials return nil @@ -158,14 +157,14 @@ func (l *loginCmd) storeCredentials(e *env, credentials *credentials) error { return stackerr.Wrap(err) } -func (l *loginCmd) authUserWithToken(e *env) error { +func (l *login) authUserWithToken(e *env) error { tokenCredentials, err := l.getTokenCredentials(e) if err != nil { return err } - apps := &apps{login: loginCmd{credentials: *tokenCredentials}} + apps := &apps{login: login{credentials: *tokenCredentials}} _, err = apps.restFetchApps(e) if err == errAuth { fmt.Fprintf(e.Err, @@ -184,7 +183,7 @@ please type "parse login" and provide a valid token for the email. return nil } -func (l *loginCmd) authUser(e *env) error { +func (l *login) authUser(e *env) error { if l.authUserWithToken(e) == nil { return nil } @@ -215,7 +214,7 @@ func (l *loginCmd) authUser(e *env) error { return errAuth } -func (l *loginCmd) helpCreateToken(e *env) { +func (l *login) helpCreateToken(e *env) { var shouldOpen string fmt.Fscanf(e.In, "%s\n", &shouldOpen) if shouldOpen == "n" { @@ -235,7 +234,7 @@ Please open %q in the browser to create a new account key. const keysURL = "https://www.parse.com/account/keys" -func (l *loginCmd) run(e *env) error { +func (l *login) run(e *env) error { fmt.Fprintf(e.Out, `Please enter the email id you used to register with Parse and an account key if you already generated it. @@ -250,7 +249,7 @@ please type: "y" to open the browser or "n" to continue: `, fmt.Fprintf(e.Out, "Account Key: ") fmt.Fscanf(e.In, "%s\n", &credentials.token) - _, err := (&apps{login: loginCmd{credentials: credentials}}).restFetchApps(e) + _, err := (&apps{login: login{credentials: credentials}}).restFetchApps(e) if err != nil { if err == errAuth { fmt.Fprintf(e.Err, `Sorry, we do not have a user with this email and account key. @@ -270,13 +269,3 @@ Please follow instructions at %s to generate a new account key. } return stackerr.Wrap(err) } - -func newLoginCmd(e *env) *cobra.Command { - l := &loginCmd{} - return &cobra.Command{ - Use: "login", - Short: "Login with your Parse credentials", - Long: `Login with your parse user name and an account key.`, - Run: runNoArgs(e, l.run), - } -} diff --git a/login_cmd_test.go b/login_test.go similarity index 96% rename from login_cmd_test.go rename to login_test.go index e97b25d..c041631 100644 --- a/login_cmd_test.go +++ b/login_test.go @@ -15,7 +15,7 @@ func TestPopulateCreds(t *testing.T) { h := newHarness(t) defer h.Stop() - l := &loginCmd{} + l := &login{} h.env.In = strings.NewReader("email\npassword\n") ensure.Nil(t, l.populateCreds(h.env)) ensure.DeepEqual(t, l.credentials.email, "email") @@ -28,7 +28,7 @@ func TestGetTokenCredentials(t *testing.T) { h := newHarness(t) defer h.Stop() - l := &loginCmd{} + l := &login{} h.env.Server = "http://api.example.com/1/" l.tokenReader = strings.NewReader( @@ -53,7 +53,7 @@ func TestAuthUserWithToken(t *testing.T) { h, _ := newAppHarness(t) defer h.Stop() - l := &loginCmd{} + l := &login{} h.env.Server = "http://api.example.org/1/" l.tokenReader = strings.NewReader( @@ -71,7 +71,7 @@ func TestUpdatedNetrcContent(t *testing.T) { h := newHarness(t) defer h.Stop() - l := &loginCmd{} + l := &login{} h.env.Server = "https://api.example.com/1/" updated, err := l.updatedNetrcContent(h.env, @@ -131,7 +131,7 @@ func TestLogin(t *testing.T) { h, _ := newAppHarness(t) defer h.Stop() - l := &loginCmd{tokenReader: strings.NewReader("")} + l := &login{tokenReader: strings.NewReader("")} h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ntoken\n")) ensure.Nil(t, l.run(h.env)) From 831a31a0381f1534b5d3f573bc6e396ab82a2a88 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 11 Aug 2015 00:21:25 +0000 Subject: [PATCH 008/106] [parse-cli] refactor code from login.go to create a 'configure token' command --- configure_cmd.go | 70 +++++++++++++++++++++++++++++++++++++++++++ configure_cmd_test.go | 36 ++++++++++++++++++++++ login.go | 40 ++----------------------- login_test.go | 28 ----------------- main.go | 1 + 5 files changed, 109 insertions(+), 66 deletions(-) create mode 100644 configure_cmd.go create mode 100644 configure_cmd_test.go diff --git a/configure_cmd.go b/configure_cmd.go new file mode 100644 index 0000000..60703e4 --- /dev/null +++ b/configure_cmd.go @@ -0,0 +1,70 @@ +package main + +import ( + "fmt" + + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type configureCmd struct { + login login +} + +func (c *configureCmd) accessToken(e *env) error { + fmt.Fprintf(e.Out, + `Please enter the email id you used to register with Parse +and an access token if you already generated it. +If you do not have an access token or would like to generate a new one, +please type: "y" to open the browser or "n" to continue: `, + ) + + c.login.helpCreateToken(e) + + var credentials credentials + fmt.Fprintf(e.Out, "Email: ") + fmt.Fscanf(e.In, "%s\n", &credentials.email) + fmt.Fprintf(e.Out, "Account Key: ") + fmt.Fscanf(e.In, "%s\n", &credentials.token) + + _, err := (&apps{login: login{credentials: credentials}}).restFetchApps(e) + if err != nil { + if err == errAuth { + fmt.Fprintf(e.Err, + `Sorry, we do not have a user with this email and access token. +Please follow instructions at %s to generate a new access token. +`, + keysURL, + ) + } else { + fmt.Fprintf(e.Err, "Unable to validate token with error:\n%s\n", err) + } + return stackerr.New("Could not store credentials. Please try again.") + } + + err = c.login.storeCredentials(e, &credentials) + if err == nil { + fmt.Fprintln(e.Out, "Successfully stored credentials.") + } + return stackerr.Wrap(err) +} + +func newConfigureCmd(e *env) *cobra.Command { + var c configureCmd + + cmd := &cobra.Command{ + Use: "configure", + Short: "Configure various Parse settings.", + Long: "Configure various Parse settings like access tokens, project type, and more.", + Run: func(c *cobra.Command, args []string) { + c.Help() + }, + } + cmd.AddCommand(&cobra.Command{ + Use: "token", + Short: "Store Parse access token on machine", + Long: "Stores Parse access token in ~/.parse/netrc.", + Run: runNoArgs(e, c.accessToken), + }) + return cmd +} diff --git a/configure_cmd_test.go b/configure_cmd_test.go new file mode 100644 index 0000000..b3929cd --- /dev/null +++ b/configure_cmd_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "io/ioutil" + "regexp" + "strings" + "testing" + + "github.com/facebookgo/ensure" +) + +func TestConfigureAcessToken(t *testing.T) { + t.Parallel() + + h, _ := newAppHarness(t) + defer h.Stop() + + c := configureCmd{login: login{tokenReader: strings.NewReader("")}} + h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ntoken\n")) + ensure.Nil(t, c.accessToken(h.env)) + ensure.DeepEqual(t, + h.Out.String(), + `Please enter the email id you used to register with Parse +and an access token if you already generated it. +If you do not have an access token or would like to generate a new one, +please type: "y" to open the browser or "n" to continue: Email: Account Key: Successfully stored credentials. +`) + + h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ninvalid\n")) + ensure.Err(t, c.accessToken(h.env), regexp.MustCompile("Please try again")) + ensure.DeepEqual(t, + h.Err.String(), + `Sorry, we do not have a user with this email and access token. +Please follow instructions at https://www.parse.com/account/account_keys to generate a new access token. +`) +} diff --git a/login.go b/login.go index 46c315a..7414262 100644 --- a/login.go +++ b/login.go @@ -14,6 +14,8 @@ import ( "github.com/skratchdot/open-golang/open" ) +const keysURL = "https://www.parse.com/account/account_keys" + type credentials struct { email string password string @@ -231,41 +233,3 @@ Please open %q in the browser to create a new account key. ) } } - -const keysURL = "https://www.parse.com/account/keys" - -func (l *login) run(e *env) error { - fmt.Fprintf(e.Out, - `Please enter the email id you used to register with Parse -and an account key if you already generated it. -If you do not have an account key or would like to generate a new one, -please type: "y" to open the browser or "n" to continue: `, - ) - l.helpCreateToken(e) - - var credentials credentials - fmt.Fprintf(e.Out, "Email: ") - fmt.Fscanf(e.In, "%s\n", &credentials.email) - fmt.Fprintf(e.Out, "Account Key: ") - fmt.Fscanf(e.In, "%s\n", &credentials.token) - - _, err := (&apps{login: login{credentials: credentials}}).restFetchApps(e) - if err != nil { - if err == errAuth { - fmt.Fprintf(e.Err, `Sorry, we do not have a user with this email and account key. -Please follow instructions at %s to generate a new account key. -`, - keysURL, - ) - } else { - fmt.Fprintf(e.Err, "Unable to validate token with error:\n%s\n", err) - } - return stackerr.New("Could not store credentials. Please try again.") - } - - err = l.storeCredentials(e, &credentials) - if err == nil { - fmt.Fprintln(e.Out, "Successfully stored credentials.") - } - return stackerr.Wrap(err) -} diff --git a/login_test.go b/login_test.go index c041631..9777ff4 100644 --- a/login_test.go +++ b/login_test.go @@ -1,7 +1,6 @@ package main import ( - "io/ioutil" "regexp" "strings" "testing" @@ -124,30 +123,3 @@ machine api.example.org password token`, ) } - -func TestLogin(t *testing.T) { - t.Parallel() - - h, _ := newAppHarness(t) - defer h.Stop() - - l := &login{tokenReader: strings.NewReader("")} - - h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ntoken\n")) - ensure.Nil(t, l.run(h.env)) - ensure.DeepEqual(t, - h.Out.String(), - `Please enter the email id you used to register with Parse -and an account key if you already generated it. -If you do not have an account key or would like to generate a new one, -please type: "y" to open the browser or "n" to continue: Email: Account Key: Successfully stored credentials. -`) - - h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ninvalid\n")) - ensure.Err(t, l.run(h.env), regexp.MustCompile("Please try again")) - ensure.DeepEqual(t, - h.Err.String(), - `Sorry, we do not have a user with this email and account key. -Please follow instructions at https://www.parse.com/account/keys to generate a new account key. -`) -} diff --git a/main.go b/main.go index 60f03b6..47c3973 100644 --- a/main.go +++ b/main.go @@ -280,6 +280,7 @@ http://parse.com`, } c.AddCommand(newAddCmd(e)) + c.AddCommand(newConfigureCmd(e)) c.AddCommand(newDefaultCmd(e)) c.AddCommand(newDeployCmd(e)) c.AddCommand(newDevelopCmd(e)) From 4ce82a4f30577853491fb0bec480e1cbaaec19e9 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 13 Aug 2015 01:53:29 +0000 Subject: [PATCH 009/106] fix url for access tokens --- configure_cmd_test.go | 2 +- login.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/configure_cmd_test.go b/configure_cmd_test.go index b3929cd..b0f6786 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -31,6 +31,6 @@ please type: "y" to open the browser or "n" to continue: Email: Account Key: Suc ensure.DeepEqual(t, h.Err.String(), `Sorry, we do not have a user with this email and access token. -Please follow instructions at https://www.parse.com/account/account_keys to generate a new access token. +Please follow instructions at https://www.parse.com/account_keys to generate a new access token. `) } diff --git a/login.go b/login.go index 7414262..e7b851d 100644 --- a/login.go +++ b/login.go @@ -14,7 +14,7 @@ import ( "github.com/skratchdot/open-golang/open" ) -const keysURL = "https://www.parse.com/account/account_keys" +const keysURL = "https://www.parse.com/account_keys" type credentials struct { email string From fb8d807147d7cfd546e7b620e8c38c4bef035f70 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 13 Aug 2015 02:15:28 +0000 Subject: [PATCH 010/106] add better error handling to deploy cmd * we do not print the same error if it repeats multiple times * add wait time between deploy retries * print the full error message each time it is different that means we will also print script errors now --- deploy_cmd.go | 67 +++++++++++++++++++++++++++++++++++++--------- deploy_cmd_test.go | 10 +++++-- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/deploy_cmd.go b/deploy_cmd.go index 7706e30..7bcf21b 100644 --- a/deploy_cmd.go +++ b/deploy_cmd.go @@ -14,6 +14,7 @@ import ( "sort" "strings" "sync" + "time" "github.com/facebookgo/errgroup" "github.com/facebookgo/jsonpipe" @@ -32,6 +33,7 @@ type deployCmd struct { Force bool Verbose bool Retries int + wait func(int) time.Duration } func (d *deployCmd) getSourceFiles( @@ -463,34 +465,75 @@ func (d *deployCmd) deploy( }, nil } +func (d *deployCmd) handleError( + n int, + err, prevErr error, + e *env, +) error { + if err == nil { + return nil + } + if n == d.Retries-1 { + return err + } + + var waitTime time.Duration + if d.wait != nil { + waitTime = d.wait(n) + } + + errStr := errorString(e, err) + if prevErr != nil { + prevErrStr := errorString(e, prevErr) + if prevErrStr == errStr { + fmt.Fprintf( + e.Err, + "Sorry, deploy failed again with same error.\nWill retry in %d seconds.\n\n", + waitTime/time.Second, + ) + time.Sleep(waitTime) + return nil + } + } + + fmt.Fprintf( + e.Err, + "Deploy failed with error:\n%s\nWill retry in %d seconds.\n\n", + errStr, + waitTime/time.Second, + ) + time.Sleep(waitTime) + return nil +} + func (d *deployCmd) run(e *env, c *client) error { - first := true + var prevErr error for i := 0; i < d.Retries; i++ { - if !first { - fmt.Fprintf(e.Err, "Deploy failed. Retrying deploy...\n\n") - } - config := c.Config - parseVersion := config.getProjectConfig().Parse.JSSDK + parseVersion := c.Config.getProjectConfig().Parse.JSSDK newDeployInfo, err := d.deploy(parseVersion, nil, false, e) if err == nil { if parseVersion == "" && newDeployInfo != nil && newDeployInfo.ParseVersion != "" { - config.getProjectConfig().Parse.JSSDK = newDeployInfo.ParseVersion - return storeProjectConfig(e, config) + c.Config.getProjectConfig().Parse.JSSDK = newDeployInfo.ParseVersion + return storeProjectConfig(e, c.Config) } return nil } - - first = false - if i == d.Retries-1 { + if err := d.handleError(i, err, prevErr, e); err != nil { return err } + prevErr = err } return nil } func newDeployCmd(e *env) *cobra.Command { - d := deployCmd{Verbose: true, Retries: 5} + d := deployCmd{ + Verbose: true, + Retries: 3, + wait: func(n int) time.Duration { return time.Duration(n) * time.Second }, + } + cmd := &cobra.Command{ Use: "deploy [app]", Short: "Deploys a Parse App", diff --git a/deploy_cmd_test.go b/deploy_cmd_test.go index 47000d0..1d15dd8 100644 --- a/deploy_cmd_test.go +++ b/deploy_cmd_test.go @@ -648,12 +648,18 @@ func TestDeployRetries(t *testing.T) { h.Err.Reset() d.Retries = 2 ensure.Err(t, d.run(h.env, &client), regexp.MustCompile("no such file or directory")) - ensure.DeepEqual(t, h.Err.String(), "Deploy failed. Retrying deploy...\n\n") + ensure.DeepEqual( + t, + h.Err.String(), + "Deploy failed with error:\nlstat cloud: no such file or directory\nWill retry in 0 seconds.\n\n", + ) h.Err.Reset() d.Retries = 5 ensure.Err(t, d.run(h.env, &client), regexp.MustCompile("no such file or directory")) - ensure.DeepEqual(t, h.Err.String(), strings.Repeat("Deploy failed. Retrying deploy...\n\n", 4)) + errStr := "Deploy failed with error:\nlstat cloud: no such file or directory\nWill retry in 0 seconds.\n\n" + errStr += strings.Repeat("Sorry, deploy failed again with same error.\nWill retry in 0 seconds.\n\n", 3) + ensure.DeepEqual(t, h.Err.String(), errStr) } func TestIgnoredFiles(t *testing.T) { From 0362634d895883787de278b1c470a0fea40a0396 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 13 Aug 2015 16:15:27 +0000 Subject: [PATCH 011/106] refactor configure token command to not ask for email --- apps.go | 2 +- apps_test.go | 2 +- configure_cmd.go | 9 +++------ configure_cmd_test.go | 16 ++++++++-------- login.go | 10 +++------- login_test.go | 21 ++++++++++----------- 6 files changed, 26 insertions(+), 34 deletions(-) diff --git a/apps.go b/apps.go index c5c9a82..46d3a65 100644 --- a/apps.go +++ b/apps.go @@ -81,10 +81,10 @@ func getAuthHeaders(credentials *credentials, headers http.Header) http.Header { if headers == nil { headers = make(http.Header) } - headers.Add("X-Parse-Email", credentials.email) if credentials.token != "" { headers.Add("X-Parse-Account-Key", credentials.token) } else { + headers.Add("X-Parse-Email", credentials.email) headers.Add("X-Parse-Password", credentials.password) } return headers diff --git a/apps_test.go b/apps_test.go index 21a9519..381a67d 100644 --- a/apps_test.go +++ b/apps_test.go @@ -53,7 +53,7 @@ func newAppHarness(t testing.TB) (*Harness, []*app) { email := r.Header.Get("X-Parse-Email") password := r.Header.Get("X-Parse-Password") token := r.Header.Get("X-Parse-Account-Key") - if !((email == "email" && password == "password") || (email == "email" && token == "token")) { + if !((email == "email" && password == "password") || (token == "token")) { return &http.Response{ StatusCode: http.StatusUnauthorized, Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect credentials"}`)), diff --git a/configure_cmd.go b/configure_cmd.go index 60703e4..461fbef 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -13,8 +13,7 @@ type configureCmd struct { func (c *configureCmd) accessToken(e *env) error { fmt.Fprintf(e.Out, - `Please enter the email id you used to register with Parse -and an access token if you already generated it. + `Please enter an access token if you already generated it. If you do not have an access token or would like to generate a new one, please type: "y" to open the browser or "n" to continue: `, ) @@ -22,16 +21,14 @@ please type: "y" to open the browser or "n" to continue: `, c.login.helpCreateToken(e) var credentials credentials - fmt.Fprintf(e.Out, "Email: ") - fmt.Fscanf(e.In, "%s\n", &credentials.email) - fmt.Fprintf(e.Out, "Account Key: ") + fmt.Fprintf(e.Out, "Access Token: ") fmt.Fscanf(e.In, "%s\n", &credentials.token) _, err := (&apps{login: login{credentials: credentials}}).restFetchApps(e) if err != nil { if err == errAuth { fmt.Fprintf(e.Err, - `Sorry, we do not have a user with this email and access token. + `Sorry, the access token you provided is not valid. Please follow instructions at %s to generate a new access token. `, keysURL, diff --git a/configure_cmd_test.go b/configure_cmd_test.go index b0f6786..92d0931 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -16,21 +16,21 @@ func TestConfigureAcessToken(t *testing.T) { defer h.Stop() c := configureCmd{login: login{tokenReader: strings.NewReader("")}} - h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ntoken\n")) + h.env.In = ioutil.NopCloser(strings.NewReader("n\ntoken\n")) ensure.Nil(t, c.accessToken(h.env)) ensure.DeepEqual(t, h.Out.String(), - `Please enter the email id you used to register with Parse -and an access token if you already generated it. + `Please enter an access token if you already generated it. If you do not have an access token or would like to generate a new one, -please type: "y" to open the browser or "n" to continue: Email: Account Key: Successfully stored credentials. -`) - +please type: "y" to open the browser or "n" to continue: Access Token: Successfully stored credentials. +`, + ) h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ninvalid\n")) ensure.Err(t, c.accessToken(h.env), regexp.MustCompile("Please try again")) ensure.DeepEqual(t, h.Err.String(), - `Sorry, we do not have a user with this email and access token. + `Sorry, the access token you provided is not valid. Please follow instructions at https://www.parse.com/account_keys to generate a new access token. -`) +`, + ) } diff --git a/login.go b/login.go index e7b851d..b5c7156 100644 --- a/login.go +++ b/login.go @@ -99,7 +99,6 @@ func (l *login) getTokenCredentials(e *env) (*credentials, error) { return nil, stackerr.Newf("could not find token for %s", server) } return &credentials{ - email: machine.Login, token: machine.Password, }, nil } @@ -120,9 +119,8 @@ func (l *login) updatedNetrcContent( } machine := tokens.FindMachine(server) if machine == nil { - machine = tokens.NewMachine(server, credentials.email, credentials.token, "") + machine = tokens.NewMachine(server, "default", credentials.token, "") } else { - machine.UpdateLogin(credentials.email) machine.UpdatePassword(credentials.token) } @@ -169,12 +167,10 @@ func (l *login) authUserWithToken(e *env) error { apps := &apps{login: login{credentials: *tokenCredentials}} _, err = apps.restFetchApps(e) if err == errAuth { - fmt.Fprintf(e.Err, + fmt.Fprintln(e.Err, `Sorry, you have an invalid token associated with the email: %q. To avoid typing the email and password everytime, -please type "parse login" and provide a valid token for the email. -`, - tokenCredentials.email, +please type "parse configure token" and provide a valid access token.`, ) } if err != nil { diff --git a/login_test.go b/login_test.go index 9777ff4..5df1bc0 100644 --- a/login_test.go +++ b/login_test.go @@ -32,13 +32,12 @@ func TestGetTokenCredentials(t *testing.T) { l.tokenReader = strings.NewReader( `machine api.example.com - login email + login default password token `, ) credentials, err := l.getTokenCredentials(h.env) ensure.Nil(t, err) - ensure.DeepEqual(t, credentials.email, "email") ensure.DeepEqual(t, credentials.token, "token") h.env.Server = "http://api.parse.com" @@ -76,26 +75,26 @@ func TestUpdatedNetrcContent(t *testing.T) { updated, err := l.updatedNetrcContent(h.env, strings.NewReader( `machine api.example.com - login email + login default password token0 machine api.example.org - login email + login default password token `, ), - &credentials{email: "email", token: "token"}, + &credentials{token: "token"}, ) ensure.Nil(t, err) ensure.DeepEqual(t, string(updated), `machine api.example.com - login email + login default password token machine api.example.org - login email + login default password token `, ) @@ -104,22 +103,22 @@ machine api.example.org updated, err = l.updatedNetrcContent(h.env, strings.NewReader( `machine api.example.com - login email + login default password token `, ), - &credentials{email: "email", token: "token"}, + &credentials{token: "token"}, ) ensure.Nil(t, err) ensure.DeepEqual(t, string(updated), `machine api.example.com - login email + login default password token machine api.example.org - login email + login default password token`, ) } From 918bf9d754ab662f91ab9da5c6f2bb1d6f435b0d Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 18 Aug 2015 00:51:38 +0000 Subject: [PATCH 012/106] api of update changed in upstream library. fix it --- ios_symbol_uploader.go | 8 +++++++- update_cmd.go | 10 ++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/ios_symbol_uploader.go b/ios_symbol_uploader.go index eec8dce..786fbd8 100644 --- a/ios_symbol_uploader.go +++ b/ios_symbol_uploader.go @@ -3,6 +3,7 @@ package main import ( "bytes" "fmt" + "net/http" "os" "os/exec" "path/filepath" @@ -177,7 +178,12 @@ func (i *iosSymbolUploader) symbolConversionTool(baseDir string, if updater == nil { if toUpdate { fmt.Fprintln(e.Out, "Fetching required resources...") - err, _ = update.New().Target(toolPath).FromUrl(osxSymbolConverterDownloadURL) + var resp *http.Response + resp, err = http.Get(osxSymbolConverterDownloadURL) + if err == nil { + defer resp.Body.Close() + err = update.Apply(resp.Body, &update.Options{TargetPath: toolPath}) + } } } else { err = updater(toolPath, osxSymbolConverterDownloadURL) diff --git a/update_cmd.go b/update_cmd.go index 59012b6..fbfd3b2 100644 --- a/update_cmd.go +++ b/update_cmd.go @@ -65,8 +65,14 @@ func (u *updateCmd) updateCLI(e *env) (bool, error) { } fmt.Fprintf(e.Out, "Downloading binary from %s.\n", downloadURL) - if err, _ := update.New().Target(exec).FromUrl(downloadURL); err != nil { - return false, stackerr.Newf("Update failed: %v", err) + resp, err := http.Get(downloadURL) + if err != nil { + return false, stackerr.Newf("Update failed with error: %v", err) + } + defer resp.Body.Close() + err = update.Apply(resp.Body, &update.Options{TargetPath: exec}) + if err != nil { + return false, stackerr.Newf("Update failed with error: %v", err) } fmt.Fprintf(e.Out, "Successfully updated binary at: %s\n", exec) return true, nil From c4113479e1d62baf9d9d9b083b927425d87a59ff Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 16 Aug 2015 17:21:04 +0000 Subject: [PATCH 013/106] check if version is supported 1) if a cli version was bad, we'll blacklist it at parse and such cli's should not be used to execute anything 2) instead of forced update, we now print a message asking to update everytime a new release is made 3) use git releases to download the new binary 4) also support linux_arm now --- main.go | 46 ++++++---------------------------- update_cmd.go | 62 ++++++++++++++++++++++++++-------------------- update_cmd_test.go | 39 ++++++++--------------------- utils.go | 19 ++++++++++++++ utils_test.go | 49 ++++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 93 deletions(-) diff --git a/main.go b/main.go index 47c3973..6cb4b46 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,6 @@ import ( "io" "net/url" "os" - "os/exec" "os/signal" "path/filepath" "runtime" @@ -26,10 +25,7 @@ const ( defaultBaseURL = "https://api.parse.com/1/" ) -var ( - autoUpdate = false - userAgent = fmt.Sprintf("parse-cli-%s-%s", runtime.GOOS, version) -) +var userAgent = fmt.Sprintf("parse-cli-%s-%s", runtime.GOOS, version) type versionCmd struct{} @@ -377,39 +373,13 @@ func main() { } e.Client = client - // autoUpdate is false for all non-production builds - // so we never auto update - // for production builds autoUpdate is true but - // we suppress auto-update iff PARSE_NOUPDATE is not set - if autoUpdate && os.Getenv("PARSE_NOUPDATE") == "" { - // Perform a best effort update - updated, err := (&updateCmd{}).updateCLI(&e) - if err != nil { - cmd := exec.Command(os.Args[0], "version") - if err := cmd.Run(); err != nil { - fmt.Fprintf(e.Out, `parse cli corrupted during update. -Please follow instructions at: - https://parse.com/apps/quickstart#cloud_code -to install a new cli.`) - os.Exit(1) - } - } - - if updated { - // Re-run the command with the updated CLI - cmd := exec.Cmd{ - Path: os.Args[0], - Args: os.Args, - Stdin: os.Stdin, - Stdout: os.Stdout, - Stderr: os.Stderr, - } - - if err := cmd.Run(); err != nil { - os.Exit(1) - } - return - } + message, err := checkIfSupported(&e, version) + if err != nil { + fmt.Fprintln(e.Err, err) + os.Exit(1) + } + if message != "" { + fmt.Fprintln(e.Err, message) } if err := rootCmd(&e).Execute(); err != nil { diff --git a/update_cmd.go b/update_cmd.go index fbfd3b2..efce3d5 100644 --- a/update_cmd.go +++ b/update_cmd.go @@ -4,9 +4,7 @@ import ( "fmt" "net/http" "net/url" - "path" "runtime" - "strings" "github.com/facebookgo/stackerr" "github.com/inconshreveable/go-update" @@ -15,50 +13,60 @@ import ( ) const ( - macCliDownloadURL = "https://parse.com/downloads/cloud_code/cli/parse-osx/latest" - unixCliDownloadURL = "https://parse.com/downloads/cloud_code/cli/parse-linux/latest" - windowsCliDownloadURL = "https://parse.com/downloads/cloud_code/cli/parse-windows/latest" + macDownload = "parse" + windowsDownload = "parse.exe" + linuxDownload = "parse_linux" + linuxArmDownload = "parse_linux_arm" + downloadURLFormat = "https://github.com/ParsePlatform/parse-cli/releases/download/release_%s/%s" ) type updateCmd struct{} -func (u *updateCmd) latestVersion(e *env, downloadURL string) (string, error) { - dURL, err := url.Parse(downloadURL) - if err != nil { - return "", stackerr.Wrap(err) +func (u *updateCmd) latestVersion(e *env) (string, error) { + v := make(url.Values) + v.Set("version", "latest") + req := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, } - resp, err := e.Client.Do(&http.Request{Method: "HEAD", URL: dURL}, nil, nil) - if err != nil { - return "", nil // if unable to fetch latest cli version, do not abort! + var res struct { + Version string `json:"version"` } - base := path.Base(resp.Header.Get("Location")) - base = strings.TrimSuffix(base, ".exe") - //parse-os-2.0.2 - splits := strings.Split(base, "-") + if _, err := e.Client.Do(req, nil, &res); err != nil { + return "", stackerr.Wrap(err) + } - return splits[len(splits)-1], nil + return res.Version, nil } func (u *updateCmd) updateCLI(e *env) (bool, error) { - downloadURL := unixCliDownloadURL - switch runtime.GOOS { - case "windows": - downloadURL = windowsCliDownloadURL - case "darwin": - downloadURL = macCliDownloadURL - } + ostype := runtime.GOOS + arch := runtime.GOARCH - latestVersion, err := u.latestVersion(e, downloadURL) + latestVersion, err := u.latestVersion(e) if err != nil { - return false, stackerr.Wrap(err) + return false, err } - if latestVersion == "" || latestVersion == version { return false, nil } + var downloadURL string + switch ostype { + case "darwin": + downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, macDownload) + case "windows": + downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, windowsDownload) + case "linux": + if arch == "arm" { + downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, linuxArmDownload) + } else { + downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, linuxDownload) + } + } + exec, err := osext.Executable() if err != nil { return false, stackerr.Wrap(err) diff --git a/update_cmd_test.go b/update_cmd_test.go index 2ba0945..77247a0 100644 --- a/update_cmd_test.go +++ b/update_cmd_test.go @@ -3,10 +3,10 @@ package main import ( "io/ioutil" "net/http" - "strings" "testing" "github.com/facebookgo/ensure" + "github.com/facebookgo/jsonpipe" "github.com/facebookgo/parse" ) @@ -17,37 +17,20 @@ func TestLatestVersion(t *testing.T) { defer h.Stop() ht := transportFunc(func(r *http.Request) (*http.Response, error) { - var result string - switch r.URL.String() { - case windowsCliDownloadURL: - result = "http://parse-cli.aws.com/hash/parse-windows-2.0.2.exe" - case unixCliDownloadURL: - result = "http://parse-cli.aws.com/hash/parse-linux-2.0.2" - case macCliDownloadURL: - result = "http://parse-cli.aws.com/hash/parse-osx-2.0.2" - } - resp := http.Response{ + ensure.DeepEqual(t, r.URL.Path, "/1/supported") + return &http.Response{ StatusCode: http.StatusOK, - Body: ioutil.NopCloser(strings.NewReader("Success!")), - } - if resp.Header == nil { - resp.Header = make(http.Header) - } - resp.Header.Set("Location", result) - return &resp, nil + Body: ioutil.NopCloser( + jsonpipe.Encode( + map[string]string{"version": "2.0.2"}, + ), + ), + }, nil }) h.env.Client = &Client{client: &parse.Client{Transport: ht}} u := new(updateCmd) - version, err := u.latestVersion(h.env, unixCliDownloadURL) + latestVersion, err := u.latestVersion(h.env) ensure.Nil(t, err) - ensure.DeepEqual(t, version, "2.0.2") - - version, err = u.latestVersion(h.env, macCliDownloadURL) - ensure.Nil(t, err) - ensure.DeepEqual(t, version, "2.0.2") - - version, err = u.latestVersion(h.env, windowsCliDownloadURL) - ensure.Nil(t, err) - ensure.DeepEqual(t, version, "2.0.2") + ensure.DeepEqual(t, latestVersion, "2.0.2") } diff --git a/utils.go b/utils.go index 05a0cdb..15028e6 100644 --- a/utils.go +++ b/utils.go @@ -3,6 +3,7 @@ package main import ( "errors" "fmt" + "net/http" "net/url" "regexp" "strconv" @@ -104,3 +105,21 @@ func getHostFromURL(urlStr string) (string, error) { } return server, nil } + +func checkIfSupported(e *env, version string) (string, error) { + v := make(url.Values) + v.Set("version", version) + req := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, + } + + var res struct { + Warning string `json:"warning"` + } + + if _, err := e.Client.Do(req, nil, &res); err != nil { + return "", stackerr.Wrap(err) + } + return res.Warning, nil +} diff --git a/utils_test.go b/utils_test.go index 174c99d..af389b1 100644 --- a/utils_test.go +++ b/utils_test.go @@ -2,10 +2,14 @@ package main import ( "crypto/rand" + "io/ioutil" + "net/http" "regexp" "testing" "github.com/facebookgo/ensure" + "github.com/facebookgo/jsonpipe" + "github.com/facebookgo/parse" ) func TestBothEmpty(t *testing.T) { @@ -83,3 +87,48 @@ func TestGetHostFromURL(t *testing.T) { host, err = getHostFromURL(urlStr) ensure.Err(t, err, regexp.MustCompile("not a valid url")) } + +func TestIsSupportedWarning(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.URL.Path, "/1/supported") + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser( + jsonpipe.Encode( + map[string]string{"warning": "please update"}, + ), + ), + }, nil + }) + h.env.Client = &Client{client: &parse.Client{Transport: ht}} + message, err := checkIfSupported(h.env, "2.0.2") + ensure.Nil(t, err) + ensure.DeepEqual(t, message, "please update") +} + +func TestIsSupportedError(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.URL.Path, "/1/supported") + return &http.Response{ + StatusCode: http.StatusBadRequest, + Body: ioutil.NopCloser( + jsonpipe.Encode( + map[string]string{"error": "not supported"}, + ), + ), + }, nil + }) + h.env.Client = &Client{client: &parse.Client{Transport: ht}} + _, err := checkIfSupported(h.env, "2.0.2") + ensure.Err(t, err, regexp.MustCompile("not supported")) +} From 194eb8efa13999301f4cb88288a9a6b77de6d8f6 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 18 Aug 2015 16:22:01 +0000 Subject: [PATCH 014/106] update version to 2.2.1 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 6cb4b46..ec0f390 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,7 @@ import ( ) const ( - version = "2.2.0" + version = "2.2.1" cloudDir = "cloud" hostingDir = "public" defaultBaseURL = "https://api.parse.com/1/" From 9830c60a8be26e011ee05040c8e992b1261bd26f Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 19 Aug 2015 04:18:24 +0000 Subject: [PATCH 015/106] fix new app creation flow with account keys * new app creation was not functional with account keys * print better error messages if account keys are not configured --- apps.go | 5 +---- configure_cmd.go | 1 + configure_cmd_test.go | 8 +++++--- login.go | 12 +++++++++--- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/apps.go b/apps.go index 46d3a65..0f8e5e1 100644 --- a/apps.go +++ b/apps.go @@ -207,13 +207,10 @@ func (a *apps) restCreateApp(e *env, appName string) (*app, error) { req := &http.Request{ Method: "POST", URL: &url.URL{Path: "apps"}, - Header: make(http.Header), + Header: getAuthHeaders(&a.login.credentials, nil), Body: ioutil.NopCloser(jsonpipe.Encode(map[string]string{"appName": appName})), } - req.Header.Add("X-Parse-Email", a.login.credentials.email) - req.Header.Add("X-Parse-Password", a.login.credentials.password) - var res app if response, err := e.Client.Do(req, nil, &res); err != nil { if response != nil && response.StatusCode == http.StatusUnauthorized { diff --git a/configure_cmd.go b/configure_cmd.go index 461fbef..7ee528f 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -14,6 +14,7 @@ type configureCmd struct { func (c *configureCmd) accessToken(e *env) error { fmt.Fprintf(e.Out, `Please enter an access token if you already generated it. + If you do not have an access token or would like to generate a new one, please type: "y" to open the browser or "n" to continue: `, ) diff --git a/configure_cmd_test.go b/configure_cmd_test.go index 92d0931..3d71085 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -21,10 +21,12 @@ func TestConfigureAcessToken(t *testing.T) { ensure.DeepEqual(t, h.Out.String(), `Please enter an access token if you already generated it. + If you do not have an access token or would like to generate a new one, -please type: "y" to open the browser or "n" to continue: Access Token: Successfully stored credentials. -`, - ) +please type: "y" to open the browser or "n" to continue: Please open "https://www.parse.com/account_keys" in the browser +and follow instructions to create an access token. +Access Token: Successfully stored credentials. +`) h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ninvalid\n")) ensure.Err(t, c.accessToken(h.env), regexp.MustCompile("Please try again")) ensure.DeepEqual(t, diff --git a/login.go b/login.go index b5c7156..5ef6df0 100644 --- a/login.go +++ b/login.go @@ -168,10 +168,10 @@ func (l *login) authUserWithToken(e *env) error { _, err = apps.restFetchApps(e) if err == errAuth { fmt.Fprintln(e.Err, - `Sorry, you have an invalid token associated with the email: %q. + `Sorry, the token you configured is not valid. To avoid typing the email and password everytime, -please type "parse configure token" and provide a valid access token.`, - ) +please type "parse configure token" and provide a valid access token. +`) } if err != nil { return stackerr.Wrap(err) @@ -216,6 +216,12 @@ func (l *login) helpCreateToken(e *env) { var shouldOpen string fmt.Fscanf(e.In, "%s\n", &shouldOpen) if shouldOpen == "n" { + fmt.Fprintf(e.Out, + `Please open %q in the browser +and follow instructions to create an access token. +`, + keysURL, + ) return } err := open.Run(keysURL) From 1c4ac05702eff53bbc96df9db098acbc174e6ebd Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 19 Aug 2015 16:39:52 +0000 Subject: [PATCH 016/106] [parse-cli] upgrade to new config format while still supporting the old format --- add.go | 68 +++++++ add_cmd.go | 93 ++++----- add_cmd_parse.go | 55 ------ add_cmd_test.go | 122 ++++++++++-- apps.go | 6 +- apps_test.go | 4 +- client.go | 41 ++-- commands.go | 63 ++++++ config.go | 184 ++++++++++++++++-- config_test.go | 46 +++++ context.go | 38 ++++ default_cmd.go | 25 +-- default_cmd_parse.go | 26 --- default_cmd_test.go | 99 ++++++++-- deploy_cmd.go | 10 +- deploy_cmd_test.go | 30 +-- develop_cmd.go | 2 +- develop_cmd_test.go | 2 +- functions_cmd.go | 18 +- functions_cmd_test.go | 2 +- git_utils.go | 129 +++++++++++++ jssdk_cmd.go | 10 +- jssdk_cmd_test.go | 75 ++++++-- legacy_config.go | 26 +++ legacy_config_test.go | 6 +- login.go | 10 +- logs_cmd.go | 6 +- logs_cmd_test.go | 8 +- main.go | 348 ++++------------------------------ main_test.go | 30 +-- new.go | 271 ++++++++++++++++++++++++++ new_cmd.go | 240 ++--------------------- new_cmd_test.go | 53 +++--- parse_config.go | 42 ++-- parse_symbol_uploader.go | 2 +- parse_symbol_uploader_test.go | 2 +- releases_cmd.go | 4 +- releases_cmd_test.go | 8 +- rollback_cmd.go | 4 +- rollback_cmd_test.go | 4 +- run_helpers.go | 115 +++++++++++ symbols_cmd.go | 2 +- triggers_cmd.go | 18 +- triggers_cmd_test.go | 2 +- update_cmd.go | 2 +- update_cmd_test.go | 2 +- utils.go | 50 ++++- utils_test.go | 4 +- version_cmd.go | 26 +++ 49 files changed, 1519 insertions(+), 914 deletions(-) create mode 100644 add.go delete mode 100644 add_cmd_parse.go create mode 100644 commands.go create mode 100644 config_test.go create mode 100644 context.go delete mode 100644 default_cmd_parse.go create mode 100644 git_utils.go create mode 100644 new.go create mode 100644 run_helpers.go create mode 100644 version_cmd.go diff --git a/add.go b/add.go new file mode 100644 index 0000000..786332a --- /dev/null +++ b/add.go @@ -0,0 +1,68 @@ +package main + +import ( + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type addCmd struct { + MakeDefault bool + apps *apps + verbose bool +} + +func (a *addCmd) addSelectedApp( + app *app, + args []string, + e *env, +) error { + switch e.Type { + case legacyParseFormat, parseFormat: + appConfig := a.getParseAppConfig(app) + return a.addSelectedParseApp(app.Name, appConfig, args, e) + } + + return stackerr.Newf("Unknown project type: %d.", e.Type) +} + +func (a *addCmd) selectApp(e *env) (*app, error) { + apps, err := a.apps.restFetchApps(e) + if err != nil { + return nil, err + } + app, err := a.apps.selectApp(apps, "Select an App to add to config: ", e) + if err != nil { + return nil, err + } + return app, nil +} + +func (a *addCmd) run(e *env, args []string) error { + + if err := a.apps.login.authUser(e); err != nil { + return err + } + app, err := a.selectApp(e) + if err != nil { + return err + } + return a.addSelectedApp(app, args, e) +} + +func newAddCmd(e *env) *cobra.Command { + a := &addCmd{ + MakeDefault: false, + apps: &apps{}, + verbose: true, + } + cmd := &cobra.Command{ + Use: "add [app]", + Short: "Adds a new Parse App to config in current Cloud Code directory", + Long: `Adds a new Parse App to config in current Cloud Code directory. +If an argument is given, the added application can also be referenced by that name.`, + Run: runWithArgs(e, a.run), + } + cmd.Flags().BoolVarP(&a.MakeDefault, "default", "d", a.MakeDefault, + "Make the selected app default") + return cmd +} diff --git a/add_cmd.go b/add_cmd.go index 9795a7d..eb8eb1c 100644 --- a/add_cmd.go +++ b/add_cmd.go @@ -1,81 +1,68 @@ package main import ( - "os" - "path/filepath" + "fmt" "github.com/facebookgo/stackerr" - "github.com/spf13/cobra" ) -type addCmd struct { - MakeDefault bool - apps *apps +func (a *addCmd) getParseAppConfig(app *app) *parseAppConfig { + return &parseAppConfig{ + ApplicationID: app.ApplicationID, + masterKey: app.MasterKey, + } } -func (a *addCmd) writeConfig( - app *app, +func (a *addCmd) addSelectedParseApp( + appName string, + appConfig *parseAppConfig, args []string, e *env, - verbose bool, ) error { config, err := configFromDir(e.Root) if err != nil { return err } + parseConfig, ok := config.(*parseConfig) + if !ok { + return stackerr.New("Invalid Cloud Code config.") + } - p := config.getProjectConfig() - if p.Type == legacy { - parseConfig, ok := config.(*parseConfig) - if !ok { - return stackerr.New("Invalid Cloud Code config.") - } - return a.writeParseConfig(parseConfig, app, args, e, verbose) + // add app to config + if _, ok := parseConfig.Applications[appName]; ok { + return stackerr.Newf("App %s has already been added", appName) } - return stackerr.Newf("Unknown project type: %s.", p.Type) -} + parseConfig.Applications[appName] = appConfig -func (a *addCmd) selectApp(e *env) (*app, error) { - apps, err := a.apps.restFetchApps(e) - if err != nil { - return nil, err - } - app, err := a.apps.selectApp(apps, "Select an App to add to config: ", e) - if err != nil { - return nil, err + if len(args) > 0 && args[0] != "" { + alias := args[0] + aliasConfig, ok := parseConfig.Applications[alias] + if !ok { + parseConfig.Applications[alias] = &parseAppConfig{Link: appName} + } + if ok && aliasConfig.getLink() != "" { + fmt.Fprintf(e.Out, "Overwriting alias: %q to point to %q\n", alias, appName) + parseConfig.Applications[alias] = &parseAppConfig{Link: appName} + } } - return app, nil -} -func (a *addCmd) run(e *env, args []string) error { - _, err := os.Lstat(filepath.Join(e.Root, legacyConfigFile)) - if os.IsNotExist(err) { - return stackerr.New("Please run add command inside a parse project.") + if a.MakeDefault { + if _, ok := parseConfig.Applications[defaultKey]; ok { + return stackerr.New(`Default key already set. To override default, use command "parse default"`) + } + parseConfig.Applications[defaultKey] = &parseAppConfig{Link: appName} } - if err := a.apps.login.authUser(e); err != nil { + + if err := storeConfig(e, parseConfig); err != nil { return err } - app, err := a.selectApp(e) - if err != nil { - return err + if a.verbose { + fmt.Fprintf(e.Out, "Written config for %q\n", appName) + if a.MakeDefault { + fmt.Fprintf(e.Out, "Set %q as default\n", appName) + } } - return a.writeConfig(app, args, e, true) -} -func newAddCmd(e *env) *cobra.Command { - a := &addCmd{ - MakeDefault: false, - apps: &apps{}, - } - cmd := &cobra.Command{ - Use: "add [app]", - Short: "Adds a new Parse App to config in current Cloud Code directory", - Long: `Adds a new Parse App to config in current Cloud Code directory. -If an argument is given, the added application can also be referenced by that name.`, - Run: runWithArgs(e, a.run), - } - cmd.Flags().BoolVarP(&a.MakeDefault, "default", "d", a.MakeDefault, - "Make the selected app default") - return cmd + return nil } diff --git a/add_cmd_parse.go b/add_cmd_parse.go deleted file mode 100644 index 8201625..0000000 --- a/add_cmd_parse.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/facebookgo/stackerr" -) - -func (a *addCmd) writeParseConfig( - parseConfig *parseConfig, - app *app, - args []string, - e *env, - verbose bool, -) error { - // add app to config - if _, ok := parseConfig.Applications[app.Name]; ok { - return stackerr.Newf("App %s has already been added", app.Name) - } - parseConfig.Applications[app.Name] = &parseAppConfig{ - ApplicationID: app.ApplicationID, - MasterKey: app.MasterKey, - } - - if len(args) > 0 && args[0] != "" { - alias := args[0] - aliasConfig, ok := parseConfig.Applications[alias] - if !ok { - parseConfig.Applications[alias] = &parseAppConfig{Link: app.Name} - } - if ok && aliasConfig.getLink() != "" { - fmt.Fprintf(e.Out, "Overwriting alias: %q to point to %q\n", alias, app.Name) - parseConfig.Applications[alias] = &parseAppConfig{Link: app.Name} - } - } - - if a.MakeDefault { - if _, ok := parseConfig.Applications[defaultKey]; ok { - return stackerr.New(`Default key already set. To override default, use command "parse default"`) - } - parseConfig.Applications[defaultKey] = &parseAppConfig{Link: app.Name} - } - - if err := storeParseConfig(e, parseConfig); err != nil { - return err - } - if verbose { - fmt.Fprintf(e.Out, "Written config for %q\n", app.Name) - if a.MakeDefault { - fmt.Fprintf(e.Out, "Set %q as default\n", app.Name) - } - } - - return nil -} diff --git a/add_cmd_test.go b/add_cmd_test.go index ab479ee..06fc794 100644 --- a/add_cmd_test.go +++ b/add_cmd_test.go @@ -15,6 +15,7 @@ import ( func newAddCmdHarness(t testing.TB) (*Harness, []*app) { h := newHarness(t) defer h.Stop() + apps := []*app{ { Name: "A", @@ -30,19 +31,24 @@ func newAddCmdHarness(t testing.TB) (*Harness, []*app) { res := map[string][]*app{"results": apps} ht := transportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/apps") - email, password := r.Header.Get("X-Parse-Email"), r.Header.Get("X-Parse-Password") - if email == "email" && password == "password" { + + email := r.Header.Get("X-Parse-Email") + password := r.Header.Get("X-Parse-Password") + token := r.Header.Get("X-Parse-Account-Key") + if !((email == "email" && password == "password") || (token == "token")) { return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect credentials"}`)), }, nil } + return &http.Response{ - StatusCode: http.StatusUnauthorized, - Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect username / password"}`)), + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), }, nil + }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} return h, apps } @@ -52,16 +58,7 @@ func newAddCmdHarnessWithConfig(t testing.TB) (*Harness, *addCmd, error) { h, _ := newAddCmdHarness(t) h.makeEmptyRoot() - if err := os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755); err != nil { - return nil, nil, err - } - - if err := ioutil.WriteFile( - filepath.Join(h.env.Root, legacyConfigFile), - []byte("{}"), - 0600); err != nil { - return nil, nil, err - } + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "B"}, false)) return h, &a, nil } @@ -72,8 +69,22 @@ func TestAddCmdGlobalMakeDefault(t *testing.T) { defer h.Stop() a.MakeDefault = true ensure.Nil(t, err) + h.env.Type = parseFormat h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) ensure.Nil(t, a.run(h.env, []string{})) + + content, err := ioutil.ReadFile(filepath.Join(h.env.Root, parseLocal)) + ensure.Nil(t, err) + ensure.DeepEqual(t, string(content), `{ + "applications": { + "A": { + "applicationId": "appId.A" + }, + "_default": { + "link": "A" + } + } +}`) } func TestAddCmdGlobalNotDefault(t *testing.T) { @@ -81,6 +92,83 @@ func TestAddCmdGlobalNotDefault(t *testing.T) { h, a, err := newAddCmdHarnessWithConfig(t) defer h.Stop() ensure.Nil(t, err) + h.env.Type = parseFormat h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) ensure.Nil(t, a.run(h.env, []string{})) + + content, err := ioutil.ReadFile(filepath.Join(h.env.Root, parseLocal)) + ensure.Nil(t, err) + ensure.DeepEqual(t, string(content), `{ + "applications": { + "A": { + "applicationId": "appId.A" + } + } +}`) +} + +// NOTE: test functionality for legacy config format + +func newLegacyAddCmdHarnessWithConfig(t testing.TB) (*Harness, *addCmd, error) { + a := addCmd{apps: &defaultApps} + + h, _ := newAddCmdHarness(t) + h.makeEmptyRoot() + + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "B"}, false)) + ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) + ensure.Nil(t, + ioutil.WriteFile( + filepath.Join(h.env.Root, legacyConfigFile), + []byte("{}"), + 0600, + ), + ) + return h, &a, nil +} + +func TestLegacyAddCmdGlobalMakeDefault(t *testing.T) { + t.Parallel() + h, a, err := newLegacyAddCmdHarnessWithConfig(t) + defer h.Stop() + a.MakeDefault = true + ensure.Nil(t, err) + h.env.Type = legacyParseFormat + h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) + ensure.Nil(t, a.run(h.env, []string{})) + + content, err := ioutil.ReadFile(filepath.Join(h.env.Root, legacyConfigFile)) + ensure.Nil(t, err) + ensure.DeepEqual(t, string(content), `{ + "global": {}, + "applications": { + "A": { + "applicationId": "appId.A" + }, + "_default": { + "link": "A" + } + } +}`) +} + +func TestLegacyAddCmdGlobalNotDefault(t *testing.T) { + t.Parallel() + h, a, err := newLegacyAddCmdHarnessWithConfig(t) + defer h.Stop() + ensure.Nil(t, err) + h.env.Type = legacyParseFormat + h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) + ensure.Nil(t, a.run(h.env, []string{})) + + content, err := ioutil.ReadFile(filepath.Join(h.env.Root, legacyConfigFile)) + ensure.Nil(t, err) + ensure.DeepEqual(t, string(content), `{ + "global": {}, + "applications": { + "A": { + "applicationId": "appId.A" + } + } +}`) } diff --git a/apps.go b/apps.go index 0f8e5e1..e97849b 100644 --- a/apps.go +++ b/apps.go @@ -105,7 +105,7 @@ func fetchAppKeys(e *env, appID string) (*app, error) { } res := &app{} - if response, err := e.Client.Do(req, nil, res); err != nil { + if response, err := e.ParseAPIClient.Do(req, nil, res); err != nil { if response.StatusCode == http.StatusUnauthorized { return nil, errAuth } @@ -128,7 +128,7 @@ func (a *apps) restFetchApps(e *env) ([]*app, error) { Results []*app `json:"results"` } - if response, err := e.Client.Do(req, nil, &res); err != nil { + if response, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { if response.StatusCode == http.StatusUnauthorized { return nil, errAuth } @@ -212,7 +212,7 @@ func (a *apps) restCreateApp(e *env, appName string) (*app, error) { } var res app - if response, err := e.Client.Do(req, nil, &res); err != nil { + if response, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { if response != nil && response.StatusCode == http.StatusUnauthorized { return nil, errAuth } diff --git a/apps_test.go b/apps_test.go index 381a67d..0aaf8d0 100644 --- a/apps_test.go +++ b/apps_test.go @@ -107,7 +107,7 @@ func newAppHarness(t testing.TB) (*Harness, []*app) { } }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} return h, apps } @@ -267,7 +267,7 @@ func TestNoPanicAppCreate(t *testing.T) { ensure.DeepEqual(t, r.URL.Path, "/1/apps") return nil, errors.New("nil response panic") }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} var apps apps app, err := apps.restCreateApp(h.env, "panic!") diff --git a/client.go b/client.go index c97b1b1..7d99a3c 100644 --- a/client.go +++ b/client.go @@ -5,14 +5,27 @@ import ( "net/url" "github.com/facebookgo/parse" + "github.com/facebookgo/stackerr" ) -// Client is the http client used by parse-cli -type Client struct { - client *parse.Client +// ParseAPIClient is the http client used by parse-cli +type ParseAPIClient struct { + apiClient *parse.Client } -func (c *Client) appendCommonHeaders(header http.Header) http.Header { +func newParseAPIClient(e *env) (*ParseAPIClient, error) { + baseURL, err := url.Parse(e.Server) + if err != nil { + return nil, stackerr.Newf("invalid server URL %q: %s", e.Server, err) + } + return &ParseAPIClient{ + apiClient: &parse.Client{ + BaseURL: baseURL, + }, + }, nil +} + +func (c *ParseAPIClient) appendCommonHeaders(header http.Header) http.Header { if header == nil { header = make(http.Header) } @@ -22,42 +35,42 @@ func (c *Client) appendCommonHeaders(header http.Header) http.Header { // Get performs a GET method call on the given url and unmarshal response into // result. -func (c *Client) Get(u *url.URL, result interface{}) (*http.Response, error) { +func (c *ParseAPIClient) Get(u *url.URL, result interface{}) (*http.Response, error) { return c.Do(&http.Request{Method: "GET", URL: u}, nil, result) } // Post performs a POST method call on the given url with the given body and // unmarshal response into result. -func (c *Client) Post(u *url.URL, body, result interface{}) (*http.Response, error) { +func (c *ParseAPIClient) Post(u *url.URL, body, result interface{}) (*http.Response, error) { return c.Do(&http.Request{Method: "POST", URL: u}, body, result) } // Put performs a PUT method call on the given url with the given body and // unmarshal response into result. -func (c *Client) Put(u *url.URL, body, result interface{}) (*http.Response, error) { +func (c *ParseAPIClient) Put(u *url.URL, body, result interface{}) (*http.Response, error) { return c.Do(&http.Request{Method: "PUT", URL: u}, body, result) } // Delete performs a DELETE method call on the given url and unmarshal response // into result. -func (c *Client) Delete(u *url.URL, result interface{}) (*http.Response, error) { +func (c *ParseAPIClient) Delete(u *url.URL, result interface{}) (*http.Response, error) { return c.Do(&http.Request{Method: "DELETE", URL: u}, nil, result) } // RoundTrip is a wrapper for parse.Client.RoundTrip -func (c *Client) RoundTrip(req *http.Request) (*http.Response, error) { +func (c *ParseAPIClient) RoundTrip(req *http.Request) (*http.Response, error) { req.Header = c.appendCommonHeaders(req.Header) - return c.client.RoundTrip(req) + return c.apiClient.RoundTrip(req) } // Do is a wrapper for parse.Client.Do -func (c *Client) Do(req *http.Request, body, result interface{}) (*http.Response, error) { +func (c *ParseAPIClient) Do(req *http.Request, body, result interface{}) (*http.Response, error) { req.Header = c.appendCommonHeaders(req.Header) - return c.client.Do(req, body, result) + return c.apiClient.Do(req, body, result) } // WithCredentials is a wrapper for parse.Client.WithCredentials -func (c *Client) WithCredentials(cr parse.Credentials) *Client { - c.client = c.client.WithCredentials(cr) +func (c *ParseAPIClient) WithCredentials(cr parse.Credentials) *ParseAPIClient { + c.apiClient = c.apiClient.WithCredentials(cr) return c } diff --git a/commands.go b/commands.go new file mode 100644 index 0000000..ebf9081 --- /dev/null +++ b/commands.go @@ -0,0 +1,63 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/spf13/cobra" +) + +func parseRootCmd(e *env) *cobra.Command { + c := &cobra.Command{ + Use: "parse", + Long: fmt.Sprintf( + `Parse Command Line Interface +Version %v +Copyright %d Parse, Inc. +http://parse.com`, + version, + time.Now().Year(), + ), + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + c.AddCommand(newAddCmd(e)) + c.AddCommand(newConfigureCmd(e)) + c.AddCommand(newDefaultCmd(e)) + c.AddCommand(newDeployCmd(e)) + c.AddCommand(newDevelopCmd(e)) + c.AddCommand(newFunctionHooksCmd(e)) + c.AddCommand(newGenerateCmd(e)) + c.AddCommand(newJsSdkCmd(e)) + c.AddCommand(newListCmd(e)) + c.AddCommand(newLogsCmd(e)) + c.AddCommand(newNewCmd(e)) + c.AddCommand(newReleasesCmd(e)) + c.AddCommand(newRollbackCmd(e)) + c.AddCommand(newSymbolsCmd(e)) + c.AddCommand(newTriggerHooksCmd(e)) + c.AddCommand(newUpdateCmd(e)) + c.AddCommand(newVersionCmd(e)) + + if len(os.Args) <= 1 { + return c + } + + commands := []string{"help"} + for _, command := range c.Commands() { + commands = append(commands, command.Name()) + } + + args := make([]string, len(os.Args)-1) + copy(args, os.Args[1:]) + + if message := makeCorrections(commands, args); message != "" { + fmt.Fprintln(e.Out, message) + } + c.SetArgs(args) + + return c +} diff --git a/config.go b/config.go index 32d7c27..3797090 100644 --- a/config.go +++ b/config.go @@ -1,6 +1,9 @@ package main import ( + "encoding/json" + "fmt" + "io/ioutil" "os" "path/filepath" @@ -8,8 +11,12 @@ import ( ) const ( - defaultKey = "_default" - legacy = iota + 1 + legacyParseFormat = iota + parseFormat + + defaultKey = "_default" + parseLocal = ".parse.local" + parseProject = ".parse.project" ) type appConfig interface { @@ -41,37 +48,120 @@ type projectConfig struct { } func getConfigFile(e *env) string { - if e.Type == legacy { + switch e.Type { + case legacyParseFormat: return filepath.Join(e.Root, legacyConfigFile) + case parseFormat: + return filepath.Join(e.Root, parseLocal) } return "" } func configFromDir(dir string) (config, error) { l, err := readLegacyConfigFile(filepath.Join(dir, legacyConfigFile)) - if err != nil { + if err != nil && !stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) { + return nil, err + } + if l != nil { // legacy config format + projectConfig := &projectConfig{ + Type: legacyParseFormat, + Parse: &parseProjectConfig{JSSDK: l.Global.ParseVersion}, + } + applications := l.Applications + if applications == nil { + applications = make(map[string]*parseAppConfig) + } + return &parseConfig{ + Applications: applications, + projectConfig: projectConfig, + }, nil + } + + canonicalize := func(err error) error { + if err == nil { + return nil + } if stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) { - return nil, stackerr.New("Command must be run in a directory containing a Parse project.") + return stackerr.New("Command must be run inside a Parse project.") } - return nil, err + return err } - projectConfig := &projectConfig{ - Type: legacy, - Parse: &parseProjectConfig{JSSDK: l.Global.ParseVersion}, + + // current config format + p, err := readProjectConfigFile(filepath.Join(dir, parseProject)) + if err != nil { + return nil, canonicalize(err) } - applications := l.Applications - if applications == nil { - applications = make(map[string]*parseAppConfig) + configFile := filepath.Join(dir, parseLocal) + switch p.Type { + case parseFormat: + c, err := readParseConfigFile(configFile) + if err != nil { + return nil, canonicalize(err) + } + if c.Applications == nil { + c.Applications = make(map[string]*parseAppConfig) + } + c.projectConfig = p + return c, nil } - return &parseConfig{ - Applications: applications, - projectConfig: projectConfig, - }, nil + + return nil, stackerr.Newf("Unknown project type: %d.", p.Type) } -func storeProjectConfig(e *env, c config) error { +func writeConfigFile(c config, path string) error { + b, err := json.MarshalIndent(c, "", " ") + if err != nil { + return stackerr.Wrap(err) + } + return stackerr.Wrap(ioutil.WriteFile(path, b, 0600)) +} + +func storeConfig(e *env, c config) error { projectType := c.getProjectConfig().Type - if projectType == legacy { + switch projectType { + case legacyParseFormat: + pc, ok := (c).(*parseConfig) + if !ok { + return stackerr.Newf("Incorrect project type: 'legacy'.") + } + lconf := &legacyConfig{Applications: pc.Applications} + lconf.Global.ParseVersion = pc.projectConfig.Parse.JSSDK + return writeLegacyConfigFile( + lconf, + filepath.Join(e.Root, legacyConfigFile), + ) + case parseFormat: + return writeConfigFile(c, filepath.Join(e.Root, parseLocal)) + } + return stackerr.Newf("Unknown project type: %d.", projectType) +} + +func readProjectConfigFile(path string) (*projectConfig, error) { + f, err := os.Open(path) + if err != nil { + return nil, stackerr.Wrap(err) + } + defer f.Close() + var p projectConfig + if err := json.NewDecoder(f).Decode(&p); err != nil { + return nil, stackerr.Newf("Config file %q is not valid JSON.", path) + } + return &p, err +} + +func writeProjectConfig(p *projectConfig, path string) error { + b, err := json.MarshalIndent(p, "", " ") + if err != nil { + return stackerr.Wrap(err) + } + return stackerr.Wrap(ioutil.WriteFile(path, b, 0600)) +} + +func storeProjectConfig(e *env, c config) error { + p := c.getProjectConfig() + switch p.Type { + case legacyParseFormat: p, ok := (c).(*parseConfig) if !ok { return stackerr.New("Invalid Cloud Code config.") @@ -82,7 +172,63 @@ func storeProjectConfig(e *env, c config) error { lconf, filepath.Join(e.Root, legacyConfigFile), ) + + case parseFormat: + return writeProjectConfig(p, filepath.Join(e.Root, parseProject)) + } + + return stackerr.Newf("Unknown project type: %d.", p.Type) +} + +func isProjectDir(cur string) bool { + _, err := os.Stat(filepath.Join(cur, parseProject)) + if err != nil { + return false + } + _, err = os.Stat(filepath.Join(cur, parseLocal)) + if err != nil { + return false + } + return true +} + +func getProjectRoot(e *env, cur string) string { + if isProjectDir(cur) { + return cur + } + + root := cur + base := filepath.Base(root) + + for base != "." && base != string(filepath.Separator) { + base = filepath.Base(root) + root = filepath.Dir(root) + if isProjectDir(root) { + return root + } + } + + return cur +} + +func printDefault(e *env, defaultApp string) error { + if defaultApp == "" { + return stackerr.New("No app is set as default app") + } + fmt.Fprintf(e.Out, "Current default app is %s\n", defaultApp) + return nil +} + +func setDefault(e *env, newDefault, defaultApp string, c config) error { + projectType := c.getProjectConfig().Type + switch projectType { + case legacyParseFormat, parseFormat: + p, ok := c.(*parseConfig) + if !ok { + return stackerr.New("Invalid Cloud Code config.") + } + return setParseDefault(e, newDefault, defaultApp, p) } - return stackerr.Newf("Unknown project type: %s.", projectType) + return stackerr.Newf("Unknown project type: %d.", projectType) } diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..4ba54f8 --- /dev/null +++ b/config_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/facebookgo/ensure" +) + +func TestGetConfigFile(t *testing.T) { + t.Parallel() + h := newHarness(t) + defer h.Stop() + + h.env.Type = legacyParseFormat + ensure.DeepEqual(t, getConfigFile(h.env), filepath.Join(h.env.Root, legacyConfigFile)) + + h.env.Type = parseFormat + ensure.DeepEqual(t, getConfigFile(h.env), filepath.Join(h.env.Root, parseLocal)) +} + +func TestConfigFromDirFormatType(t *testing.T) { + t.Parallel() + h := newHarness(t) + h.makeEmptyRoot() + defer h.Stop() + + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + + c, err := configFromDir(h.env.Root) + ensure.Nil(t, err) + + ensure.DeepEqual(t, c.getProjectConfig().Type, parseFormat) + + ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) + ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.env.Root, legacyConfigFile), + []byte("{}"), + 0600), + ) + c, err = configFromDir(h.env.Root) + ensure.Nil(t, err) + + ensure.DeepEqual(t, c.getProjectConfig().Type, legacyParseFormat) +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..a773706 --- /dev/null +++ b/context.go @@ -0,0 +1,38 @@ +package main + +import "github.com/facebookgo/parse" + +type context struct { + Config config + AppName string + AppConfig appConfig +} + +func newContext(e *env, appName string) (*context, error) { + config, err := configFromDir(e.Root) + if err != nil { + return nil, err + } + + app, err := config.app(appName) + if err != nil { + return nil, err + } + + masterKey, err := app.getMasterKey(e) + if err != nil { + return nil, err + } + e.ParseAPIClient = e.ParseAPIClient.WithCredentials( + parse.MasterKey{ + ApplicationID: app.getApplicationID(), + MasterKey: masterKey, + }, + ) + + return &context{ + AppName: appName, + AppConfig: app, + Config: config, + }, nil +} diff --git a/default_cmd.go b/default_cmd.go index 1e471b0..1cba6b2 100644 --- a/default_cmd.go +++ b/default_cmd.go @@ -1,33 +1,12 @@ package main import ( - "fmt" - "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) type defaultCmd struct{} -func (d *defaultCmd) printDefault(e *env, defaultApp string) error { - if defaultApp == "" { - return stackerr.New("No app is set as default app") - } - fmt.Fprintf(e.Out, "Current default app is %s\n", defaultApp) - return nil -} - -func (d *defaultCmd) setDefault(e *env, newDefault, defaultApp string, c config) error { - if c.getProjectConfig().Type == legacy { - p, ok := c.(*parseConfig) - if !ok { - return stackerr.New("Invalid Cloud Code config.") - } - return d.setParseDefault(e, newDefault, defaultApp, p) - } - return stackerr.Newf("Project type not configured.") -} - func (d *defaultCmd) run(e *env, args []string) error { var newDefault string if len(args) > 1 { @@ -50,9 +29,9 @@ func (d *defaultCmd) run(e *env, args []string) error { switch newDefault { case "": - return d.printDefault(e, defaultApp) + return printDefault(e, defaultApp) default: - return d.setDefault(e, newDefault, defaultApp, config) + return setDefault(e, newDefault, defaultApp, config) } } diff --git a/default_cmd_parse.go b/default_cmd_parse.go deleted file mode 100644 index 662bdef..0000000 --- a/default_cmd_parse.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/facebookgo/stackerr" -) - -func (d *defaultCmd) setParseDefault(e *env, newDefault, defaultApp string, parseConfig *parseConfig) error { - apps := parseConfig.Applications - if _, ok := apps[newDefault]; !ok { - parseConfig.pprintApps(e) - return stackerr.Newf("Invalid application name \"%s\". Please select from the valid applications printed above.", newDefault) - } - - if defaultApp == "" { - apps[defaultKey] = &parseAppConfig{Link: newDefault} - } else { - apps[defaultKey].Link = newDefault - } - if err := storeParseConfig(e, parseConfig); err != nil { - return err - } - fmt.Fprintf(e.Out, "Default app set to %s.\n", newDefault) - return nil -} diff --git a/default_cmd_test.go b/default_cmd_test.go index 47c34a5..449f87c 100644 --- a/default_cmd_test.go +++ b/default_cmd_test.go @@ -1,32 +1,35 @@ package main import ( + "io/ioutil" + "os" + "path/filepath" "regexp" "testing" "github.com/facebookgo/ensure" ) -func createNewProject(t testing.TB, h *Harness) (*parseConfig, error) { - var n newCmd - ensure.Nil(t, n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) +func newDefaultParseConfig(t testing.TB, h *Harness) *parseConfig { c, err := configFromDir(h.env.Root) - if err != nil { - return nil, err - } + ensure.Nil(t, err) + config, ok := c.(*parseConfig) ensure.True(t, ok) config.Applications = map[string]*parseAppConfig{"first": {}, "second": {}, defaultKey: {Link: "first"}} - config.projectConfig = &projectConfig{Type: legacy, Parse: &parseProjectConfig{}} - return config, storeParseConfig(h.env, config) + config.projectConfig = &projectConfig{Type: legacyParseFormat, Parse: &parseProjectConfig{}} + ensure.Nil(t, storeConfig(h.env, config)) + + return config } func newDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConfig) { h := newHarness(t) h.makeEmptyRoot() - config, err := createNewProject(t, h) - ensure.Nil(t, err) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + + config := newDefaultParseConfig(t, h) h.Out.Reset() d := &defaultCmd{} @@ -35,15 +38,16 @@ func newDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConfig) { func TestPrintDefaultNotSet(t *testing.T) { t.Parallel() - h, d, _ := newDefaultCmdHarness(t) + h, _, _ := newDefaultCmdHarness(t) defer h.Stop() - ensure.Err(t, d.printDefault(h.env, ""), regexp.MustCompile("No app is set as default app")) + ensure.Err(t, printDefault(h.env, ""), regexp.MustCompile("No app is set as default app")) } func TestPrintDefaultSet(t *testing.T) { - h, d, _ := newDefaultCmdHarness(t) + t.Parallel() + h, _, _ := newDefaultCmdHarness(t) defer h.Stop() - d.printDefault(h.env, "first") + printDefault(h.env, "first") ensure.DeepEqual(t, h.Out.String(), `Current default app is first `) @@ -51,9 +55,13 @@ func TestPrintDefaultSet(t *testing.T) { func TestSetDefaultInvalid(t *testing.T) { t.Parallel() - h, d, config := newDefaultCmdHarness(t) + h, _, config := newDefaultCmdHarness(t) defer h.Stop() - ensure.Err(t, d.setDefault(h.env, "invalid", "", config), regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`)) + ensure.Err( + t, + setDefault(h.env, "invalid", "", config), + regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`), + ) ensure.DeepEqual(t, h.Out.String(), `The following apps are associated with cloud code in the current directory: * first @@ -63,17 +71,17 @@ func TestSetDefaultInvalid(t *testing.T) { func TestSetDefault(t *testing.T) { t.Parallel() - h, d, config := newDefaultCmdHarness(t) + h, _, config := newDefaultCmdHarness(t) defer h.Stop() - ensure.Nil(t, d.setDefault(h.env, "first", "", config)) + ensure.Nil(t, setDefault(h.env, "first", "", config)) ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") } func TestSetDefaultWithApp(t *testing.T) { t.Parallel() - h, d, config := newDefaultCmdHarness(t) + h, _, config := newDefaultCmdHarness(t) defer h.Stop() - ensure.Nil(t, d.setDefault(h.env, "first", "first", config)) + ensure.Nil(t, setDefault(h.env, "first", "first", config)) ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") } @@ -83,3 +91,54 @@ func TestDefaultRunMoreArgs(t *testing.T) { defer h.Stop() ensure.Err(t, d.run(h.env, []string{"one", "two"}), regexp.MustCompile(`unexpected arguments, only an optional app name is expected:`)) } + +// NOTE: testing for legacy format +func newLegacyDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConfig) { + h := newHarness(t) + h.makeEmptyRoot() + + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) + ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.env.Root, legacyConfigFile), + []byte("{}"), + 0600), + ) + + config := newDefaultParseConfig(t, h) + h.Out.Reset() + + d := &defaultCmd{} + return h, d, config +} + +func TestLegacySetDefaultInvalid(t *testing.T) { + t.Parallel() + h, _, config := newLegacyDefaultCmdHarness(t) + defer h.Stop() + ensure.Err( + t, + setDefault(h.env, "invalid", "", config), + regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`), + ) + ensure.DeepEqual(t, h.Out.String(), + `The following apps are associated with cloud code in the current directory: +* first + second +`) +} + +func TestLegacySetDefault(t *testing.T) { + t.Parallel() + h, _, config := newLegacyDefaultCmdHarness(t) + defer h.Stop() + ensure.Nil(t, setDefault(h.env, "first", "", config)) + ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") +} + +func TestLegacySetDefaultWithApp(t *testing.T) { + t.Parallel() + h, _, config := newLegacyDefaultCmdHarness(t) + defer h.Stop() + ensure.Nil(t, setDefault(h.env, "first", "first", config)) + ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") +} diff --git a/deploy_cmd.go b/deploy_cmd.go index 7bcf21b..c46fc8e 100644 --- a/deploy_cmd.go +++ b/deploy_cmd.go @@ -191,7 +191,7 @@ func (d *deployCmd) uploadFile(filename, endpoint string, e *env, Version string `json:"version"` } - if _, err := e.Client.Do(req, nil, &res); err != nil { + if _, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { return "", stackerr.Wrap(err) } @@ -317,7 +317,7 @@ func (d *deployCmd) makeNewRelease(info *deployInfo, e *env) (deployInfo, error) u := url.URL{ Path: "deploy", } - _, err := e.Client.Post(&u, info, &res) + _, err := e.ParseAPIClient.Post(&u, info, &res) if err != nil { return res, stackerr.Wrap(err) } @@ -326,7 +326,7 @@ func (d *deployCmd) makeNewRelease(info *deployInfo, e *env) (deployInfo, error) func (d *deployCmd) getPrevDeplInfo(e *env) (*deployInfo, error) { prevDeplInfo := &deployInfo{} - if _, err := e.Client.Get(&url.URL{Path: "deploy"}, prevDeplInfo); err != nil { + if _, err := e.ParseAPIClient.Get(&url.URL{Path: "deploy"}, prevDeplInfo); err != nil { return nil, stackerr.Wrap(err) } legacy := len(prevDeplInfo.Checksums.Cloud) == 0 && @@ -341,7 +341,7 @@ func (d *deployCmd) getPrevDeplInfo(e *env) (*deployInfo, error) { Checksums map[string]string `json:"checksums,omitempty"` Versions map[string]string `json:"userFiles,omitempty"` } - if _, err := e.Client.Get(&url.URL{Path: "deploy"}, &res); err != nil { + if _, err := e.ParseAPIClient.Get(&url.URL{Path: "deploy"}, &res); err != nil { return nil, stackerr.Wrap(err) } prevDeplInfo.ReleaseName = res.ReleaseName @@ -506,7 +506,7 @@ func (d *deployCmd) handleError( return nil } -func (d *deployCmd) run(e *env, c *client) error { +func (d *deployCmd) run(e *env, c *context) error { var prevErr error for i := 0; i < d.Retries; i++ { parseVersion := c.Config.getProjectConfig().Parse.JSSDK diff --git a/deploy_cmd_test.go b/deploy_cmd_test.go index 1d15dd8..734d85d 100644 --- a/deploy_cmd_test.go +++ b/deploy_cmd_test.go @@ -105,7 +105,7 @@ func setupForDeploy(t testing.TB, info *deployInfo) *Harness { }, nil } }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} return h } @@ -189,7 +189,7 @@ func TestUploadFileHttpError(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(`{"error": "something is wrong"}`)), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} dirRoot := filepath.Join(h.env.Root, "cloud") _, err := d.uploadFile(filepath.Join(dirRoot, "main.js"), "uploads", h.env, func(name string) string { return "main.js" }) @@ -210,7 +210,7 @@ func TestUploadFileMalformed(t *testing.T) { }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} dirRoot := filepath.Join(h.env.Root, "cloud") _, err := d.uploadFile(filepath.Join(dirRoot, "main.js"), "uploads", h.env, func(name string) string { return "main.js" }) @@ -242,7 +242,7 @@ func TestMakeNewRelease(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &info))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} res, err := d.makeNewRelease(&deployInfo{}, h.env) ensure.Nil(t, err) ensure.DeepEqual(t, info, res) @@ -261,7 +261,7 @@ func TestMakeNewReleaseError(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(`{"error": "something is wrong"}`)), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} _, err := d.makeNewRelease(&deployInfo{}, h.env) ensure.Err(t, err, regexp.MustCompile("something is wrong")) } @@ -291,7 +291,7 @@ func TestGetPrevDeplInfo(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, info))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} res, err := d.getPrevDeplInfo(h.env) ensure.Nil(t, err) ensure.DeepEqual(t, res, info) @@ -323,7 +323,7 @@ func TestGetPrevDeplInfoLegacy(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, info))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} res, err := d.getPrevDeplInfo(h.env) ensure.Nil(t, err) ensure.DeepEqual(t, res, &deployInfo{ @@ -351,7 +351,7 @@ func TestGetPrevDeplInfoError(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(`{"error": "something is wrong"}`)), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} _, err := d.getPrevDeplInfo(h.env) ensure.Err(t, err, regexp.MustCompile("something is wrong")) } @@ -382,7 +382,7 @@ func TestUploadSourceFilesChanged(t *testing.T) { }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} var d deployCmd checksums, versions, err := d.uploadSourceFiles(u) @@ -636,18 +636,18 @@ func TestDeployRetries(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, info))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} d := &deployCmd{Retries: 1} - client := client{Config: defaulParseConfig} - client.Config.getProjectConfig().Parse.JSSDK = "latest" + ctx := context{Config: defaultParseConfig} + ctx.Config.getProjectConfig().Parse.JSSDK = "latest" - ensure.Err(t, d.run(h.env, &client), regexp.MustCompile("no such file or directory")) + ensure.Err(t, d.run(h.env, &ctx), regexp.MustCompile("no such file or directory")) ensure.DeepEqual(t, h.Err.String(), "") h.Err.Reset() d.Retries = 2 - ensure.Err(t, d.run(h.env, &client), regexp.MustCompile("no such file or directory")) + ensure.Err(t, d.run(h.env, &ctx), regexp.MustCompile("no such file or directory")) ensure.DeepEqual( t, h.Err.String(), @@ -656,7 +656,7 @@ func TestDeployRetries(t *testing.T) { h.Err.Reset() d.Retries = 5 - ensure.Err(t, d.run(h.env, &client), regexp.MustCompile("no such file or directory")) + ensure.Err(t, d.run(h.env, &ctx), regexp.MustCompile("no such file or directory")) errStr := "Deploy failed with error:\nlstat cloud: no such file or directory\nWill retry in 0 seconds.\n\n" errStr += strings.Repeat("Sorry, deploy failed again with same error.\nWill retry in 0 seconds.\n\n", 3) ensure.DeepEqual(t, h.Err.String(), errStr) diff --git a/develop_cmd.go b/develop_cmd.go index a19fd80..7831375 100644 --- a/develop_cmd.go +++ b/develop_cmd.go @@ -92,7 +92,7 @@ func (d *developCmd) handleError(e *env, err error, sleep func(time.Duration)) e return err } -func (d *developCmd) run(e *env, c *client) error { +func (d *developCmd) run(e *env, c *context) error { first := make(chan struct{}) go d.contDeploy(e, deployFunc((&deployCmd{Verbose: d.Verbose}).deploy), diff --git a/develop_cmd_test.go b/develop_cmd_test.go index dc65e00..61adca4 100644 --- a/develop_cmd_test.go +++ b/develop_cmd_test.go @@ -50,7 +50,7 @@ func TestContDeployConfigErr(t *testing.T) { 0600, ), ) - h.env.Type = legacy + h.env.Type = legacyParseFormat deployer := deployFunc(func(parseVersion string, prevDeplInfo *deployInfo, diff --git a/functions_cmd.go b/functions_cmd.go index deb46fb..14dfda4 100644 --- a/functions_cmd.go +++ b/functions_cmd.go @@ -57,7 +57,7 @@ func readFunctionParams(e *env) (*functionHook, error) { const defaultFunctionsURL = "/1/hooks/functions" -func (h *functionHooksCmd) functionHooksCreate(e *env, client *client) error { +func (h *functionHooksCmd) functionHooksCreate(e *env, ctx *context) error { params, err := readFunctionParams(e) if err != nil { return err @@ -67,7 +67,7 @@ func (h *functionHooksCmd) functionHooksCreate(e *env, client *client) error { if err != nil { return err } - _, err = e.Client.Post(functionsURL, params, &res) + _, err = e.ParseAPIClient.Post(functionsURL, params, &res) if err != nil { return err } @@ -83,7 +83,7 @@ func (h *functionHooksCmd) functionHooksCreate(e *env, client *client) error { return nil } -func (h *functionHooksCmd) functionHooksRead(e *env, client *client) error { +func (h *functionHooksCmd) functionHooksRead(e *env, ctx *context) error { u := defaultFunctionsURL var function *functionHook if !h.All { @@ -102,7 +102,7 @@ func (h *functionHooksCmd) functionHooksRead(e *env, client *client) error { var res struct { Results []*functionHook `json:"results,omitempty"` } - _, err = e.Client.Get(functionsURL, &res) + _, err = e.ParseAPIClient.Get(functionsURL, &res) if err != nil { return err } @@ -125,7 +125,7 @@ func (h *functionHooksCmd) functionHooksRead(e *env, client *client) error { return nil } -func (h *functionHooksCmd) functionHooksUpdate(e *env, client *client) error { +func (h *functionHooksCmd) functionHooksUpdate(e *env, ctx *context) error { params, err := readFunctionParams(e) if err != nil { return err @@ -136,7 +136,7 @@ func (h *functionHooksCmd) functionHooksUpdate(e *env, client *client) error { return err } - _, err = e.Client.Put(functionsURL, &functionHook{URL: params.URL}, &res) + _, err = e.ParseAPIClient.Put(functionsURL, &functionHook{URL: params.URL}, &res) if err != nil { return err } @@ -152,7 +152,7 @@ func (h *functionHooksCmd) functionHooksUpdate(e *env, client *client) error { return nil } -func (h *functionHooksCmd) functionHooksDelete(e *env, client *client) error { +func (h *functionHooksCmd) functionHooksDelete(e *env, ctx *context) error { params, err := readFunctionName(e) if err != nil { return err @@ -169,7 +169,7 @@ func (h *functionHooksCmd) functionHooksDelete(e *env, client *client) error { var res functionHook if getConfirmation(confirmMessage, e) { - _, err = e.Client.Put(functionsURL, map[string]interface{}{"__op": "Delete"}, &res) + _, err = e.ParseAPIClient.Put(functionsURL, map[string]interface{}{"__op": "Delete"}, &res) if err != nil { return err } @@ -182,7 +182,7 @@ func (h *functionHooksCmd) functionHooksDelete(e *env, client *client) error { return nil } -func (h *functionHooksCmd) functionHooks(e *env, c *client) error { +func (h *functionHooksCmd) functionHooks(e *env, c *context) error { hp := *h hp.All = true return hp.functionHooksRead(e, c) diff --git a/functions_cmd_test.go b/functions_cmd_test.go index 9167a3e..24a9b2e 100644 --- a/functions_cmd_test.go +++ b/functions_cmd_test.go @@ -95,7 +95,7 @@ func newFunctionsHarness(t testing.TB) *Harness { Body: ioutil.NopCloser(jsonpipe.Encode(body)), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} return h } diff --git a/git_utils.go b/git_utils.go new file mode 100644 index 0000000..87e10aa --- /dev/null +++ b/git_utils.go @@ -0,0 +1,129 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/facebookgo/stackerr" +) + +type gitInfo struct { + repo string + authToken string + remote string + description string +} + +func (g *gitInfo) checkAbort(e *env) error { + _, err := exec.LookPath("git") + if err != nil { + return stackerr.New(`Unable to locate "git". +Please install "git" and ensure that you are able to run "git help" from the command prompt.`, + ) + } + + _, err = os.Lstat(filepath.Join(e.Root, ".git")) + if os.IsNotExist(err) { + return stackerr.Newf("%s is not a git repository", e.Root) + } + return nil +} + +func (g *gitInfo) runCmd(cmd *exec.Cmd, msg string) error { + err := cmd.Run() + if err == nil { + return nil + } + if _, ok := err.(*exec.ExitError); ok { + if msg != "" { + return stackerr.New(msg) + } + return stackerr.Newf("error executing: %q", strings.Join(cmd.Args, " ")) + } + return stackerr.Wrap(err) +} + +func (g *gitInfo) getDescription() string { + if g.description != "" { + return g.description + } + return fmt.Sprintf("committed by parse-cli at: %s", time.Now()) +} + +func (g *gitInfo) isDirty(e *env) (bool, error) { + if err := g.checkAbort(e); err != nil { + return false, err + } + + cmd := exec.Command("git", "status", "--porcelain") + var cmdOut, cmdErr bytes.Buffer + cmd.Stdout = &cmdOut + cmd.Stderr = &cmdErr + err := g.runCmd(cmd, fmt.Sprintf("Unable to determine the status of git repo: %s", g.repo)) + if err != nil { + fmt.Fprintln(e.Err, cmdErr.String()) + return false, err + } + + if cmdOut.Len() == 0 { + return false, nil + } + + fmt.Fprintf(e.Out, "Status of git repo: %q:\n%s", g.repo, cmdOut.String()) + return true, nil +} + +func (g *gitInfo) commit(e *env) error { + if err := g.checkAbort(e); err != nil { + return err + } + + cmd := exec.Command("git", "add", "-A", ".") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) + if err != nil { + return err + } + + cmd = exec.Command("git", "commit", "-a", "-m", g.getDescription()) + return g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) +} + +func (g *gitInfo) push(e *env, force bool) error { + if err := g.checkAbort(e); err != nil { + return err + } + command := []string{"push"} + if force { + command = append(command, "-f") + } + command = append(command, + fmt.Sprintf("https://:%s@%s", g.authToken, g.remote), + "master", + ) + cmd := exec.Command("git", command...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return g.runCmd( + cmd, + fmt.Sprintf("Unable to push to git remote: %s", g.remote), + ) +} + +func (g *gitInfo) clone(gitURL, path string) error { + _, err := exec.LookPath("git") + if err != nil { + return stackerr.New(`Unable to locate "git". +Please install "git" and ensure that you are able to run "git help" from the command prompt.`, + ) + } + + cmd := exec.Command("git", "clone", gitURL, path) + return g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) +} diff --git a/jssdk_cmd.go b/jssdk_cmd.go index 1d081e9..90c053b 100644 --- a/jssdk_cmd.go +++ b/jssdk_cmd.go @@ -23,14 +23,14 @@ func (j *jsSDKCmd) getAllJSSdks(e *env) ([]string, error) { Path: "jsVersions", } var response jsSDKVersion - if _, err := e.Client.Get(u, &response); err != nil { + if _, err := e.ParseAPIClient.Get(u, &response); err != nil { return nil, stackerr.Wrap(err) } sort.Sort(naturalStrings(response.JS)) return response.JS, nil } -func (j *jsSDKCmd) printVersions(e *env, c *client) error { +func (j *jsSDKCmd) printVersions(e *env, c *context) error { allVersions, err := j.getAllJSSdks(e) if err != nil { return err @@ -47,7 +47,7 @@ func (j *jsSDKCmd) printVersions(e *env, c *client) error { return nil } -func (j *jsSDKCmd) getVersion(e *env, c *client) error { +func (j *jsSDKCmd) getVersion(e *env, c *context) error { if c.Config.getProjectConfig().Parse.JSSDK == "" { return stackerr.New("JavaScript SDK version not set for this project.") } @@ -59,7 +59,7 @@ func (j *jsSDKCmd) getVersion(e *env, c *client) error { return nil } -func (j *jsSDKCmd) setVersion(e *env, c *client) error { +func (j *jsSDKCmd) setVersion(e *env, c *context) error { allVersions, err := j.getAllJSSdks(e) if err != nil { return err @@ -109,7 +109,7 @@ func useLatestJSSDK(e *env) error { return storeProjectConfig(e, config) } -func (j *jsSDKCmd) run(e *env, c *client, args []string) error { +func (j *jsSDKCmd) run(e *env, c *context, args []string) error { switch { case j.all: return j.printVersions(e, c) diff --git a/jssdk_cmd_test.go b/jssdk_cmd_test.go index bfe3d06..7657b19 100644 --- a/jssdk_cmd_test.go +++ b/jssdk_cmd_test.go @@ -23,7 +23,7 @@ func newJsSdkHarness(t testing.TB) *Harness { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} return h } @@ -36,28 +36,24 @@ func newJsSdkHarnessError(t testing.TB) *Harness { Body: ioutil.NopCloser(strings.NewReader(`{"error":"something is wrong"}`)), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} return h } -func newJsSdkHarnessWithConfig(t testing.TB) (*Harness, *client) { +func newJsSdkHarnessWithConfig(t testing.TB) (*Harness, *context) { h := newJsSdkHarness(t) h.makeEmptyRoot() - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, configDir), 0755)) - path := filepath.Join(h.env.Root, legacyConfigFile) - ensure.Nil(t, ioutil.WriteFile(path, - []byte(`{ - "global": { - "parseVersion" : "1.2.9" - } - }`), - 0600)) - cfg, err := configFromDir(h.env.Root) + + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + h.Out.Reset() + + c, err := configFromDir(h.env.Root) ensure.Nil(t, err) - config, ok := (cfg).(*parseConfig) + + config, ok := (c).(*parseConfig) ensure.True(t, ok) - c := &client{Config: config} - return h, c + + return h, &context{Config: config} } func TestGetAllJSVersions(t *testing.T) { @@ -88,7 +84,7 @@ func TestPrintVersions(t *testing.T) { ensure.DeepEqual(t, h.Out.String(), ` 1.2.11 1.2.10 -* 1.2.9 + 1.2.9 1.2.8 0.2.0 `) @@ -107,7 +103,7 @@ func TestSetVersionNoneSelected(t *testing.T) { h := newHarness(t) defer h.Stop() - c := &client{Config: defaulParseConfig} + c := &context{Config: defaultParseConfig} var j jsSDKCmd c.Config.getProjectConfig().Parse.JSSDK = "1.2.1" @@ -128,4 +124,47 @@ func TestSetValidVersion(t *testing.T) { j := jsSDKCmd{newVersion: "1.2.11"} ensure.Nil(t, j.setVersion(h.env, c)) ensure.DeepEqual(t, h.Out.String(), "Current JavaScript SDK version is 1.2.11\n") + + content, err := ioutil.ReadFile(filepath.Join(h.env.Root, parseProject)) + ensure.Nil(t, err) + ensure.DeepEqual(t, string(content), `{ + "project_type": 1, + "parse": { + "jssdk": "1.2.11" + } +}`) +} + +// NOTE: testing for legacy config format +func newLegacyJsSdkHarnessWithConfig(t testing.TB) (*Harness, *context) { + h := newJsSdkHarness(t) + h.makeEmptyRoot() + + ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, configDir), 0755)) + path := filepath.Join(h.env.Root, legacyConfigFile) + ensure.Nil(t, ioutil.WriteFile(path, + []byte(`{ + "global": { + "parseVersion" : "1.2.9" + } + }`), + 0600)) + + c, err := configFromDir(h.env.Root) + ensure.Nil(t, err) + + config, ok := (c).(*parseConfig) + ensure.True(t, ok) + + return h, &context{Config: config} +} + +func TestLegacySetValidVersion(t *testing.T) { + t.Parallel() + + h, c := newLegacyJsSdkHarnessWithConfig(t) + defer h.Stop() + j := jsSDKCmd{newVersion: "1.2.11"} + ensure.Nil(t, j.setVersion(h.env, c)) + ensure.DeepEqual(t, h.Out.String(), "Current JavaScript SDK version is 1.2.11\n") } diff --git a/legacy_config.go b/legacy_config.go index c3c390f..d025453 100644 --- a/legacy_config.go +++ b/legacy_config.go @@ -34,9 +34,35 @@ func readLegacyConfigFile(path string) (*legacyConfig, error) { } func writeLegacyConfigFile(c *legacyConfig, path string) error { + // if config directory does not exist yet, first create it + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return stackerr.Wrap(err) + } + b, err := json.MarshalIndent(c, "", " ") if err != nil { return stackerr.Wrap(err) } return stackerr.Wrap(ioutil.WriteFile(path, b, 0600)) } + +func getLegacyProjectRoot(e *env, cur string) string { + if _, err := os.Stat(filepath.Join(cur, legacyConfigFile)); err == nil { + return cur + } + + root := cur + base := filepath.Base(root) + + for base != "." && base != string(filepath.Separator) { + base = filepath.Base(root) + root = filepath.Dir(root) + if base == "cloud" || base == "public" || base == "config" { + if _, err := os.Stat(filepath.Join(root, legacyConfigFile)); err == nil { + return root + } + } + } + + return cur +} diff --git a/legacy_config_test.go b/legacy_config_test.go index 0c6ec1c..869e94b 100644 --- a/legacy_config_test.go +++ b/legacy_config_test.go @@ -9,9 +9,9 @@ import ( "github.com/facebookgo/ensure" ) -var defaulParseConfig = &parseConfig{ +var defaultParseConfig = &parseConfig{ projectConfig: &projectConfig{ - Type: legacy, + Type: legacyParseFormat, Parse: &parseProjectConfig{}, }, } @@ -103,7 +103,7 @@ func TestConfigMissing(t *testing.T) { defer os.RemoveAll(dir) c, err := configFromDir(dir) ensure.True(t, c == nil) - ensure.Err(t, err, regexp.MustCompile("Command must be run in a directory containing a Parse project.")) + ensure.Err(t, err, regexp.MustCompile("Command must be run inside a Parse project.")) } func TestConfigGlobalMalformed(t *testing.T) { diff --git a/login.go b/login.go index 5ef6df0..448c2d3 100644 --- a/login.go +++ b/login.go @@ -169,8 +169,7 @@ func (l *login) authUserWithToken(e *env) error { if err == errAuth { fmt.Fprintln(e.Err, `Sorry, the token you configured is not valid. -To avoid typing the email and password everytime, -please type "parse configure token" and provide a valid access token. + `) } if err != nil { @@ -187,8 +186,13 @@ func (l *login) authUser(e *env) error { } apps := &apps{} + fmt.Fprintln( + e.Out, + `To avoid typing the email and password everytime, +please type "parse configure token" and provide a valid access token. - fmt.Fprintln(e.Out, "Please log in to Parse using your email and password.") +Please log in to Parse using your email and password.`, + ) for i := 0; i < numRetries; i++ { err := l.populateCreds(e) if err != nil { diff --git a/logs_cmd.go b/logs_cmd.go index c6b8a37..7a7b13b 100644 --- a/logs_cmd.go +++ b/logs_cmd.go @@ -29,7 +29,7 @@ type logsCmd struct { level string } -func (l *logsCmd) run(e *env, c *client) error { +func (l *logsCmd) run(e *env, c *context) error { level := strings.ToUpper(l.level) if level != "INFO" && level != "ERROR" { return stackerr.Newf("invalid level: %q", l.level) @@ -64,7 +64,7 @@ func (l *logsCmd) run(e *env, c *client) error { return nil } -func (l *logsCmd) round(e *env, c *client, startTime *parseTime) (*parseTime, error) { +func (l *logsCmd) round(e *env, c *context, startTime *parseTime) (*parseTime, error) { v := make(url.Values) v.Set("n", fmt.Sprint(l.num)) v.Set("level", l.level) @@ -82,7 +82,7 @@ func (l *logsCmd) round(e *env, c *client, startTime *parseTime) (*parseTime, er RawQuery: v.Encode(), } var rows []logResponse - if _, err := e.Client.Get(u, &rows); err != nil { + if _, err := e.ParseAPIClient.Get(u, &rows); err != nil { return nil, stackerr.Wrap(err) } // logs come back in reverse diff --git a/logs_cmd_test.go b/logs_cmd_test.go index c307bde..e1a63c2 100644 --- a/logs_cmd_test.go +++ b/logs_cmd_test.go @@ -34,8 +34,8 @@ func TestLogWithoutFollow(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} - err := l.run(h.env, &client{}) + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + err := l.run(h.env, &context{}) ensure.Nil(t, err) ensure.DeepEqual(t, h.Out.String(), "baz\nfoo bar\n") } @@ -100,8 +100,8 @@ func TestLogWithFollow(t *testing.T) { } }() - h.env.Client = &Client{client: &parse.Client{Transport: ht}} - err := l.run(h.env, &client{}) + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + err := l.run(h.env, &context{}) close(stop) ensure.Err(t, err, regexp.MustCompile(`parse: error with status=500 and body="a"`)) diff --git a/main.go b/main.go index ec0f390..ea5e4c8 100644 --- a/main.go +++ b/main.go @@ -1,20 +1,13 @@ package main import ( - "errors" "fmt" "io" - "net/url" "os" "os/signal" - "path/filepath" "runtime" - "time" "github.com/facebookgo/clock" - "github.com/facebookgo/errgroup" - "github.com/facebookgo/parse" - "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -27,82 +20,17 @@ const ( var userAgent = fmt.Sprintf("parse-cli-%s-%s", runtime.GOOS, version) -type versionCmd struct{} - -func (c *versionCmd) run(e *env) error { - fmt.Fprintln(e.Out, version) - return nil -} - -func newVersionCmd(e *env) *cobra.Command { - var c versionCmd - cmd := &cobra.Command{ - Use: "version", - Short: "Gets the Command Line Tools version", - Long: `Gets the Command Line Tools version.`, - Run: runNoArgs(e, c.run), - Aliases: []string{"cliversion"}, - } - return cmd -} - -// errorString returns the error string with our without the stack trace -// depending on the environment variable. this exists because we want plain -// messages for end users, but when we're working on the CLI we want the stack -// trace for debugging. -func errorString(e *env, err error) string { - type hasUnderlying interface { - HasUnderlying() error - } - - parseErr := func(err error) error { - if apiErr, ok := err.(*parse.Error); ok { - return errors.New(apiErr.Message) - } - return err - } - - lastErr := func(err error) error { - if serr, ok := err.(*stackerr.Error); ok { - if errs := stackerr.Underlying(serr); len(errs) != 0 { - err = errs[len(errs)-1] - } - } else { - if eu, ok := err.(hasUnderlying); ok { - err = eu.HasUnderlying() - } - } - - return parseErr(err) - } - - if !e.ErrorStack { - if merr, ok := err.(errgroup.MultiError); ok { - var multiError []error - for _, ierr := range []error(merr) { - multiError = append(multiError, lastErr(ierr)) - } - err = errgroup.MultiError(multiError) - } else { - err = lastErr(err) - } - return parseErr(err).Error() - } - - return err.Error() -} - type env struct { - Root string // project root - Server string // parse api server - Type int // project type - ErrorStack bool - Out io.Writer - Err io.Writer - In io.Reader - Exit func(int) - Clock clock.Clock - Client *Client + Root string // project root + Server string // parse api server + Type int // project type + ErrorStack bool + Out io.Writer + Err io.Writer + In io.Reader + Exit func(int) + Clock clock.Clock + ParseAPIClient *ParseAPIClient } type client struct { @@ -111,231 +39,8 @@ type client struct { AppConfig appConfig } -func newParseClient(e *env) (*Client, error) { - baseURL, err := url.Parse(e.Server) - if err != nil { - return nil, stackerr.Newf("invalid server URL %q: %s", e.Server, err) - } - return &Client{ - client: &parse.Client{ - BaseURL: baseURL, - }, - }, nil -} - -func newClient(e *env, appName string) (*client, error) { - config, err := configFromDir(e.Root) - if err != nil { - return nil, err - } - - app, err := config.app(appName) - if err != nil { - return nil, err - } - - masterKey, err := app.getMasterKey(e) - if err != nil { - return nil, err - } - e.Client = e.Client.WithCredentials( - parse.MasterKey{ - ApplicationID: app.getApplicationID(), - MasterKey: masterKey, - }) - - return &client{ - AppName: appName, - AppConfig: app, - Config: config, - }, nil -} - -type cobraRun func(cmd *cobra.Command, args []string) - -// runNoArgs wraps a run function that shouldn't get any arguments. -func runNoArgs(e *env, f func(*env) error) cobraRun { - return func(cmd *cobra.Command, args []string) { - if len(args) != 0 { - fmt.Fprintf(e.Err, "unexpected arguments:%+v\n\n", args) - cmd.Help() - e.Exit(1) - } - if err := f(e); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) - e.Exit(1) - } - } -} - -// runWithArgs wraps a run function that can access arguments to cobraCmd -func runWithArgs(e *env, f func(*env, []string) error) cobraRun { - return func(cmd *cobra.Command, args []string) { - if err := f(e, args); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) - e.Exit(1) - } - } -} - -// runWithClient wraps a run function that should get an app, when the default is -// picked from the config in the current working directory. -func runWithClient(e *env, f func(*env, *client) error) cobraRun { - return func(cmd *cobra.Command, args []string) { - app := defaultKey - if len(args) > 1 { - fmt.Fprintf( - e.Err, - "unexpected arguments, only an optional app name is expected:%+v\n\n", - args, - ) - cmd.Help() - e.Exit(1) - } - if len(args) == 1 { - app = args[0] - } - cl, err := newClient(e, app) - if err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) - e.Exit(1) - } - if err := f(e, cl); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) - e.Exit(1) - } - } -} - -// runWithAppClient wraps a run function that should get an app -func runWithAppClient(e *env, f func(*env, *client) error) cobraRun { - return func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Fprintf(e.Err, "please provide an app name\n\n") - cmd.Help() - e.Exit(1) - } - if len(args) > 1 { - fmt.Fprintf( - e.Err, - "unexpected arguments, only an app name is expected:%+v\n\n", - args, - ) - cmd.Help() - e.Exit(1) - } - app := args[0] - cl, err := newClient(e, app) - if err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) - e.Exit(1) - } - if err := f(e, cl); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) - e.Exit(1) - } - } -} - -// runWithArgsClient wraps a run function that should get an app, whee the default is -// picked from the config in the current working directory. It also passes args to the -// runner function -func runWithArgsClient(e *env, f func(*env, *client, []string) error) cobraRun { - return func(cmd *cobra.Command, args []string) { - app := defaultKey - if len(args) > 1 { - app = args[0] - args = args[1:] - } - cl, err := newClient(e, app) - if err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) - e.Exit(1) - } - if err := f(e, cl, args); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) - e.Exit(1) - } - } -} - -func rootCmd(e *env) *cobra.Command { - c := &cobra.Command{ - Use: "parse", - Long: fmt.Sprintf( - `Parse Command Line Interface -Version %v -Copyright %d Parse, Inc. -http://parse.com`, - version, - time.Now().Year(), - ), - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, - } - - c.AddCommand(newAddCmd(e)) - c.AddCommand(newConfigureCmd(e)) - c.AddCommand(newDefaultCmd(e)) - c.AddCommand(newDeployCmd(e)) - c.AddCommand(newDevelopCmd(e)) - c.AddCommand(newFunctionHooksCmd(e)) - c.AddCommand(newGenerateCmd(e)) - c.AddCommand(newJsSdkCmd(e)) - c.AddCommand(newListCmd(e)) - c.AddCommand(newLogsCmd(e)) - c.AddCommand(newNewCmd(e)) - c.AddCommand(newReleasesCmd(e)) - c.AddCommand(newRollbackCmd(e)) - c.AddCommand(newSymbolsCmd(e)) - c.AddCommand(newTriggerHooksCmd(e)) - c.AddCommand(newUpdateCmd(e)) - c.AddCommand(newVersionCmd(e)) - - if len(os.Args) <= 1 { - return c - } - - commands := []string{"help"} - for _, command := range c.Commands() { - commands = append(commands, command.Name()) - } - - args := make([]string, len(os.Args)-1) - copy(args, os.Args[1:]) - - if message := makeCorrections(commands, args); message != "" { - fmt.Fprintln(e.Out, message) - } - c.SetArgs(args) - - return c -} - -func getProjectRoot(e *env, cur string) string { - if _, err := os.Stat(filepath.Join(cur, legacyConfigFile)); err == nil { - return cur - } - - root := cur - base := filepath.Base(root) - - for base != "." && base != string(filepath.Separator) { - base = filepath.Base(root) - root = filepath.Dir(root) - if base == "cloud" || base == "public" || base == "config" { - if _, err := os.Stat(filepath.Join(root, legacyConfigFile)); err == nil { - return root - } - } - } - - return cur -} - func main() { - // some parts of apps_cmd.go are unable to handle + // some parts of apps.go are unable to handle // interrupts, this logic ensures we exit on system interrupts interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) @@ -360,18 +65,34 @@ func main() { fmt.Fprintf(e.Err, "Failed to get current directory:\n%s\n", err) os.Exit(1) } - e.Root = getProjectRoot(&e, cur) + root := getProjectRoot(&e, cur) + if isProjectDir(root) { + e.Root = root + config, err := configFromDir(root) + if err != nil { + fmt.Fprintln(e.Err, err) + os.Exit(1) + } + e.Type = config.getProjectConfig().Type + } else { + e.Type = legacyParseFormat + e.Root = getLegacyProjectRoot(&e, cur) + } + } + if e.Type != legacyParseFormat && e.Type != parseFormat { + fmt.Fprintf(e.Err, "Unknown project type %d.\n", e.Type) + os.Exit(1) } - e.Type = legacy + if e.Server == "" { e.Server = defaultBaseURL } - client, err := newParseClient(&e) + apiClient, err := newParseAPIClient(&e) if err != nil { fmt.Fprintln(e.Err, err) os.Exit(1) } - e.Client = client + e.ParseAPIClient = apiClient message, err := checkIfSupported(&e, version) if err != nil { @@ -382,7 +103,12 @@ func main() { fmt.Fprintln(e.Err, message) } - if err := rootCmd(&e).Execute(); err != nil { + var rootCmd *cobra.Command + switch e.Type { + case legacyParseFormat, parseFormat: + rootCmd = parseRootCmd(&e) + } + if err := rootCmd.Execute(); err != nil { // Error is already printed in Execute() os.Exit(1) } diff --git a/main_test.go b/main_test.go index 202123c..8b9c36f 100644 --- a/main_test.go +++ b/main_test.go @@ -60,10 +60,10 @@ func newHarness(t testing.TB) *Harness { Clock: clock.NewMock(), } te.env = &env{ - Out: &te.Out, - Err: &te.Err, - Clock: te.Clock, - Client: &Client{client: &parse.Client{}}, + Out: &te.Out, + Err: &te.Err, + Clock: te.Clock, + ParseAPIClient: &ParseAPIClient{apiClient: &parse.Client{}}, } return &te } @@ -109,7 +109,7 @@ func TestRootCommand(t *testing.T) { t.Parallel() h := newHarness(t) defer h.Stop() - rootCmd(h.env) + parseRootCmd(h.env) } func TestErrorStringWithoutStack(t *testing.T) { @@ -194,7 +194,7 @@ func TestRunWithAppNonProjectDir(t *testing.T) { ensure.StringContains( t, h.Err.String(), - "Command must be run in a directory containing a Parse project", + "Command must be run inside a Parse project.", ) } @@ -224,7 +224,7 @@ func TestRunWithAppNamed(t *testing.T) { }, } h.makeWithConfig(jsonStr(t, c)) - r := runWithClient(h.env, func(e *env, c *client) error { + r := runWithClient(h.env, func(e *env, c *context) error { ensure.NotNil(t, c) return nil }) @@ -243,7 +243,7 @@ func TestRunWithDefault(t *testing.T) { }, } h.makeWithConfig(jsonStr(t, c)) - r := runWithClient(h.env, func(e *env, c *client) error { + r := runWithClient(h.env, func(e *env, c *context) error { ensure.NotNil(t, c) return nil }) @@ -265,7 +265,7 @@ func TestRunWithAppError(t *testing.T) { const message = "hello world" func() { defer ensure.PanicDeepEqual(t, exitCode(1)) - r := runWithClient(h.env, func(e *env, c *client) error { + r := runWithClient(h.env, func(e *env, c *context) error { ensure.NotNil(t, c) return errors.New(message) }) @@ -276,7 +276,7 @@ func TestRunWithAppError(t *testing.T) { func TestNewClientInvalidServerURL(t *testing.T) { t.Parallel() - c, err := newParseClient(&env{Server: ":"}) + c, err := newParseAPIClient(&env{Server: ":"}) ensure.True(t, c == nil) ensure.Err(t, err, regexp.MustCompile("invalid server URL")) } @@ -307,15 +307,15 @@ func TestGetProjectRoot(t *testing.T) { ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, "parse", "public"), 0755)) ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, "parse", "cloud", "other", "config"), 0755)) - ensure.DeepEqual(t, getProjectRoot(h.env, h.env.Root), h.env.Root) + ensure.DeepEqual(t, getLegacyProjectRoot(h.env, h.env.Root), h.env.Root) - ensure.DeepEqual(t, getProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "config")), filepath.Join(h.env.Root, "parse")) + ensure.DeepEqual(t, getLegacyProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "config")), filepath.Join(h.env.Root, "parse")) - ensure.DeepEqual(t, getProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "cloud")), filepath.Join(h.env.Root, "parse")) + ensure.DeepEqual(t, getLegacyProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "cloud")), filepath.Join(h.env.Root, "parse")) - ensure.DeepEqual(t, getProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "public")), filepath.Join(h.env.Root, "parse")) + ensure.DeepEqual(t, getLegacyProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "public")), filepath.Join(h.env.Root, "parse")) - ensure.DeepEqual(t, getProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "cloud", "other")), filepath.Join(h.env.Root, "parse")) + ensure.DeepEqual(t, getLegacyProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "cloud", "other")), filepath.Join(h.env.Root, "parse")) } func TestStackErrorString(t *testing.T) { diff --git a/new.go b/new.go new file mode 100644 index 0000000..fd56a2a --- /dev/null +++ b/new.go @@ -0,0 +1,271 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/facebookgo/parse" + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type newCmd struct { + addApplication bool +} + +var newProjectFiles = []struct { + dirname, filename, content string +}{ + {"cloud", "main.js", sampleSource}, + {"public", "index.html", sampleHTML}, +} + +func (n *newCmd) curlCommand(app *app) string { + return fmt.Sprintf( + `curl -X POST \ + -H "X-Parse-Application-Id: %s" \ + -H "X-Parse-REST-API-Key: %s" \ + -H "Content-Type: application/json" \ + -d '{}' \ + https://api.parse.com/1/functions/hello +`, + app.ApplicationID, + app.RestKey, + ) +} + +func (n *newCmd) cloudCodeHelpMessage(e *env, app *app) string { + return fmt.Sprintf( + `Your Cloud Code has been created at %s. +Next, you might want to deploy this code with "parse deploy". +This includes a "Hello world" cloud function, so once you deploy +you can test that it works, with: + +%s +`, + e.Root, + n.curlCommand(app), + ) +} + +func (n *newCmd) getCloudCodeDir(e *env, appName string, isNew bool) (string, error) { + var cloudCodeDir string + fmt.Fprintf(e.Out, + `Awesome! Now it's time to setup some Cloud Code for the app: %q, +Next we will create a directory to hold your Cloud Code. +Please enter the name to use for this directory, +or hit ENTER to use %q as the directory name. + +Directory Name: `, + appName, + appName, + ) + + fmt.Scanf("%s\n", &cloudCodeDir) + cloudCodeDir = strings.TrimSpace(cloudCodeDir) + if cloudCodeDir == "" { + cloudCodeDir = appName + } + + helpMsg := "Next time, please select the same app you selected now." + if isNew { + helpMsg = `Next time, please select to add Cloud Code to an 'existing' app +and select the app you just created.` + } + + configFile := filepath.Join(e.Root, cloudCodeDir, legacyConfigFile) + if _, err := os.Lstat(configFile); err == nil { + return "", stackerr.Newf( + `Sorry, we are unable to create Cloud Code at %s. +It seems that you already have Cloud Code at %s. +Please run "parse new" again. +%s +Please choose a different name for your Cloud Code directory, +so it does not conflict with any other Cloud Code in the current directory. +`, + cloudCodeDir, + cloudCodeDir, + helpMsg, + ) + } + + cloudCodeDirInfo, err := os.Stat(filepath.Join(e.Root, cloudCodeDir)) + if err == nil { + if !cloudCodeDirInfo.IsDir() { + return "", stackerr.Newf(`Sorry, we are unable to create Cloud Code at %s. +In the current directory a file named: %s already exists. +Please run "parse new" again. +%s +Please choose a different name for your Cloud Code directory, +so it does not conflict with any other Cloud Code in the current directory. +`, + cloudCodeDir, + cloudCodeDir, + helpMsg, + ) + + } + return "", nil + } + if !os.IsNotExist(err) { + return "", stackerr.Wrap(err) + } + return cloudCodeDir, nil +} + +func (n *newCmd) createConfigWithContent(path, content string) error { + file, err := os.OpenFile( + path, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, + 0600, + ) + if err != nil && !os.IsExist(err) { + return stackerr.Wrap(err) + } + defer file.Close() + if _, err := file.WriteString(content); err != nil { + return stackerr.Wrap(err) + } + if err := file.Close(); err != nil { + return stackerr.Wrap(err) + } + return nil +} + +func (n *newCmd) promptCreateNewApp(e *env) (string, error) { + msg := `"new" and "existing" are the only valid options. +Please try again ...` + + var decision string + for i := 0; i < 3; i++ { + fmt.Fprintf(e.Out, + `Would you like to create a new app, or add Cloud Code to an existing app? +Type "(n)ew" or "(e)xisting": `, + ) + fmt.Fscanf(e.In, "%s\n", &decision) + decision = strings.ToLower(decision) + + if decision == "new" || decision == "n" || decision == "existing" || decision == "e" { + return decision, nil + } + fmt.Fprintln(e.Err, msg) + } + return "", stackerr.New(msg) +} + +func (n *newCmd) setupSample(e *env, app *app, isNew bool) error { + found := isProjectDir(getProjectRoot(e, e.Root)) + if !found { + root := getLegacyProjectRoot(e, e.Root) + _, err := os.Lstat(filepath.Join(root, legacyConfigFile)) + found = err == nil + } + if found { + return stackerr.New( + `Detected that you are already inside a Parse project. +Please refrain from creating a Parse project inside another Parse project. +`, + ) + } + + switch e.Type { + case parseFormat: + return n.cloneSampleCloudCode(e, app, isNew) + } + return stackerr.Newf("Unknown project type: %d", e.Type) +} + +func (n *newCmd) configureSample( + add *addCmd, + app *app, + args []string, + e *env, +) error { + if err := add.addSelectedApp(app, args, e); err != nil { + return err + } + + // at this point user has already set a default app for the project + // use its properties to fetch latest jssdk version and set it in config + config, err := configFromDir(e.Root) + if err != nil { + return err + } + defaultApp, err := config.app(config.getDefaultApp()) + if err != nil { + return err + } + masterKey, err := defaultApp.getMasterKey(e) + if err != nil { + return err + } + e.ParseAPIClient = e.ParseAPIClient.WithCredentials( + parse.MasterKey{ + ApplicationID: defaultApp.getApplicationID(), + MasterKey: masterKey, + }, + ) + + if e.Type == parseFormat { + return useLatestJSSDK(e) + } + return nil +} + +func (n *newCmd) run(e *env) error { + apps := &apps{} + addCmd := &addCmd{MakeDefault: true, apps: apps} + + if err := apps.login.authUser(e); err != nil { + return err + } + var ( + app *app + err error + ) + + decision, err := n.promptCreateNewApp(e) + if err != nil { + return err + } + isNew := false + + switch decision { + case "new", "n": + isNew = true + app, err = apps.createApp(e) + if err != nil { + return err + } + case "existing", "e": + app, err = addCmd.selectApp(e) + if err != nil { + return err + } + } + + e.Type = parseFormat + + if err := n.setupSample(e, app, isNew); err != nil { + return err + } + if err := n.configureSample(addCmd, app, nil, e); err != nil { + return err + } + + fmt.Fprintf(e.Out, n.cloudCodeHelpMessage(e, app)) + return nil +} + +func newNewCmd(e *env) *cobra.Command { + nc := newCmd{addApplication: true} + cmd := &cobra.Command{ + Use: "new", + Short: "Creates a new Parse app and adds Cloud Code to an existing Parse app", + Long: `Creates a new Parse app and adds Cloud Code to an existing Parse app.`, + Run: runNoArgs(e, nc.run), + } + return cmd +} diff --git a/new_cmd.go b/new_cmd.go index 0d5eb83..106a7ba 100644 --- a/new_cmd.go +++ b/new_cmd.go @@ -1,121 +1,13 @@ package main import ( - "errors" "fmt" "os" "path/filepath" - "strings" - "github.com/facebookgo/parse" "github.com/facebookgo/stackerr" - "github.com/spf13/cobra" ) -type newCmd struct { - addApplication bool -} - -var newProjectFiles = []struct { - dirname, filename, content string -}{ - {"cloud", "main.js", sampleSource}, - {"public", "index.html", sampleHTML}, -} - -func (n *newCmd) curlCommand(app *app) string { - return fmt.Sprintf( - `curl -X POST \ - -H "X-Parse-Application-Id: %s" \ - -H "X-Parse-REST-API-Key: %s" \ - -H "Content-Type: application/json" \ - -d '{}' \ - https://api.parse.com/1/functions/hello -`, - app.ApplicationID, - app.RestKey, - ) -} - -func (n *newCmd) cloudCodeHelpMessage(e *env, app *app) string { - return fmt.Sprintf( - `Your Cloud Code has been created at %s. -Next, you might want to deploy this code with "parse deploy". -This includes a "Hello world" cloud function, so once you deploy -you can test that it works, with: - -%s -`, - e.Root, - n.curlCommand(app), - ) -} - -func (n *newCmd) getCloudCodeDir(e *env, appName string, isNew bool) (string, error) { - var cloudCodeDir string - fmt.Fprintf(e.Out, - `Awesome! Now it's time to setup some Cloud Code for the app: %q, -Next we will create a directory to hold your Cloud Code. -Please enter the name to use for this directory, -or hit ENTER to use %q as the directory name. - -Directory Name: `, - appName, - appName, - ) - - fmt.Scanf("%s\n", &cloudCodeDir) - cloudCodeDir = strings.TrimSpace(cloudCodeDir) - if cloudCodeDir == "" { - cloudCodeDir = appName - } - - helpMsg := "Next time, please select the same app you selected now." - if isNew { - helpMsg = `Next time, please select to add Cloud Code to an 'existing' app -and select the app you just created.` - } - - configFile := filepath.Join(e.Root, cloudCodeDir, legacyConfigFile) - if _, err := os.Lstat(configFile); err == nil { - return "", stackerr.Newf( - `Sorry, we are unable to create Cloud Code at %s. -It seems that you already have Cloud Code at %s. -Please run "parse new" again. -%s -Please choose a different name for your Cloud Code directory, -so it does not conflict with any other Cloud Code in the current directory. -`, - cloudCodeDir, - cloudCodeDir, - helpMsg, - ) - } - - cloudCodeDirInfo, err := os.Stat(filepath.Join(e.Root, cloudCodeDir)) - if err == nil { - if !cloudCodeDirInfo.IsDir() { - return "", stackerr.Newf(`Sorry, we are unable to create Cloud Code at %s. -In the current directory a file named: %s already exists. -Please run "parse new" again. -%s -Please choose a different name for your Cloud Code directory, -so it does not conflict with any other Cloud Code in the current directory. -`, - cloudCodeDir, - cloudCodeDir, - helpMsg, - ) - - } - return "", nil - } - if !os.IsNotExist(err) { - return "", stackerr.Wrap(err) - } - return cloudCodeDir, nil -} - func (n *newCmd) cloneSampleCloudCode(e *env, app *app, isNew bool) error { cloudCodeDir, err := n.getCloudCodeDir(e, app.Name, isNew) if err != nil { @@ -131,23 +23,25 @@ func (n *newCmd) cloneSampleCloudCode(e *env, app *app, isNew bool) error { return stackerr.Wrap(err) } - if err := os.Mkdir(filepath.Join(e.Root, configDir), 0755); err != nil { - return stackerr.Wrap(err) - } - file, err := os.OpenFile( - filepath.Join(e.Root, legacyConfigFile), - os.O_RDWR|os.O_CREATE|os.O_TRUNC, - 0600, + err = n.createConfigWithContent( + filepath.Join(e.Root, parseProject), + fmt.Sprintf( + `{ + "project_type" : %d, + "parse": {"jssdk":""} +}`, + parseFormat, + ), ) - if err != nil && !os.IsExist(err) { - return stackerr.Wrap(err) - } - defer file.Close() - if _, err := file.WriteString("{}"); err != nil { - return stackerr.Wrap(err) + if err != nil { + return err } - if err := file.Close(); err != nil { - return stackerr.Wrap(err) + err = n.createConfigWithContent( + filepath.Join(e.Root, parseLocal), + "{}", + ) + if err != nil { + return err } for _, info := range newProjectFiles { @@ -180,105 +74,5 @@ func (n *newCmd) cloneSampleCloudCode(e *env, app *app, isNew bool) error { } } } - fmt.Fprintf(e.Out, n.cloudCodeHelpMessage(e, app)) return nil } - -func (n *newCmd) shouldCreateNewApp(e *env) string { - var decision string - fmt.Fprintf(e.Out, - `Would you like to create a new app, or add Cloud Code to an existing app? -Type "(n)ew" or "(e)xisting": `) - fmt.Fscanf(e.In, "%s\n", &decision) - return strings.ToLower(decision) -} - -func (n *newCmd) setupSample(e *env, app *app, isNew bool) error { - _, err := os.Lstat( - filepath.Join(e.Root, legacyConfigFile), - ) - if err == nil { - return stackerr.New(`Detected that you are already inside a Parse project. -Please refrain from creating a Parse project inside another Parse project. -`) - } - - return n.cloneSampleCloudCode(e, app, isNew) -} - -func (n *newCmd) configureSample( - add *addCmd, - app *app, - e *env, - args []string) error { - if err := add.writeConfig(app, args, e, false); err != nil { - return err - } - - // at this point user has already set a default app for the project - // use its properties to fetch latest jssdk version and set it in config - config, err := configFromDir(e.Root) - if err != nil { - return err - } - defaultApp, err := config.app(config.getDefaultApp()) - if err != nil { - return err - } - masterKey, err := defaultApp.getMasterKey(e) - if err != nil { - return err - } - e.Client = e.Client.WithCredentials( - parse.MasterKey{ - ApplicationID: defaultApp.getApplicationID(), - MasterKey: masterKey, - }, - ) - return useLatestJSSDK(e) -} - -func (n *newCmd) run(e *env) error { - apps := &apps{} - addCmd := &addCmd{MakeDefault: true, apps: apps} - - if err := apps.login.authUser(e); err != nil { - return err - } - var ( - app *app - err error - ) - isNew := false - switch n.shouldCreateNewApp(e) { - case "new", "n": - isNew = true - app, err = apps.createApp(e) - if err != nil { - return err - } - case "existing", "e": - app, err = addCmd.selectApp(e) - if err != nil { - return err - } - default: - return errors.New("`new` and `existing` are the only valid options") - } - - if err := n.setupSample(e, app, isNew); err != nil { - return err - } - return n.configureSample(addCmd, app, e, nil) -} - -func newNewCmd(e *env) *cobra.Command { - nc := newCmd{addApplication: true} - cmd := &cobra.Command{ - Use: "new", - Short: "Creates a new Parse app and adds Cloud Code to an existing Parse app", - Long: `Creates a new Parse app and adds Cloud Code to an existing Parse app.`, - Run: runNoArgs(e, nc.run), - } - return cmd -} diff --git a/new_cmd_test.go b/new_cmd_test.go index feeb067..eec3da1 100644 --- a/new_cmd_test.go +++ b/new_cmd_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -31,9 +32,6 @@ func TestNewCmdDirs(t *testing.T) { _, err = os.Stat(filepath.Join(h.env.Root, newProjectFile.dirname)) ensure.Nil(t, err) } - - _, err = os.Stat(filepath.Join(h.env.Root, configDir)) - ensure.Nil(t, err) } func TestNewCmdContent(t *testing.T) { @@ -52,30 +50,23 @@ func TestNewCmdContent(t *testing.T) { ensure.DeepEqual(t, string(content), newProjectFile.content) } - content, err := ioutil.ReadFile(filepath.Join(h.env.Root, legacyConfigFile)) + content, err := ioutil.ReadFile(filepath.Join(h.env.Root, parseProject)) ensure.Nil(t, err) - ensure.DeepEqual(t, string(content), "{}") -} - -func TestNewCmdConfigExists(t *testing.T) { - t.Parallel() - - h, n := newNewCmdHarness(t) - defer h.Stop() - - ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, "test", configDir), 0755)) - ensure.Nil(t, - ioutil.WriteFile(filepath.Join(h.env.Root, "test", legacyConfigFile), - []byte("{}"), - 0600, + ensure.DeepEqual(t, + string(content), + fmt.Sprintf( + `{ + "project_type" : %d, + "parse": {"jssdk":""} +}`, + parseFormat, ), ) - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - ensure.Err(t, - n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false), - regexp.MustCompile("unable to create Cloud Code at test."), - ) + content, err = ioutil.ReadFile(filepath.Join(h.env.Root, parseLocal)) + ensure.Nil(t, err) + ensure.DeepEqual(t, string(content), "{}") + } func TestCurlCommand(t *testing.T) { @@ -100,7 +91,17 @@ func TestShouldCreateNewApp(t *testing.T) { n := &newCmd{} - h.env.In = ioutil.NopCloser(strings.NewReader("decide")) - decision := n.shouldCreateNewApp(h.env) - ensure.DeepEqual(t, decision, "decide") + h.env.In = ioutil.NopCloser(strings.NewReader("new")) + decision, err := n.promptCreateNewApp(h.env) + ensure.Nil(t, err) + ensure.DeepEqual(t, decision, "new") + + h.env.In = ioutil.NopCloser(strings.NewReader("existing")) + decision, err = n.promptCreateNewApp(h.env) + ensure.Nil(t, err) + ensure.DeepEqual(t, decision, "existing") + + h.env.In = ioutil.NopCloser(strings.NewReader("other")) + _, err = n.promptCreateNewApp(h.env) + ensure.Err(t, err, regexp.MustCompile("are the only valid options")) } diff --git a/parse_config.go b/parse_config.go index 930433d..5abacb7 100644 --- a/parse_config.go +++ b/parse_config.go @@ -1,8 +1,9 @@ package main import ( + "encoding/json" "fmt" - "path/filepath" + "os" "sort" "github.com/facebookgo/stackerr" @@ -136,15 +137,34 @@ func (c *parseConfig) pprintApps(e *env) { } } -func storeParseConfig(e *env, c *parseConfig) error { - projectType := c.projectConfig.Type - if projectType == legacy { - lconf := &legacyConfig{Applications: c.Applications} - lconf.Global.ParseVersion = c.projectConfig.Parse.JSSDK - return writeLegacyConfigFile( - lconf, - filepath.Join(e.Root, legacyConfigFile), - ) +func readParseConfigFile(path string) (*parseConfig, error) { + f, err := os.Open(path) + if err != nil { + return nil, stackerr.Wrap(err) + } + defer f.Close() + var c parseConfig + if err := json.NewDecoder(f).Decode(&c); err != nil { + return nil, stackerr.Newf("Config file %q is not valid JSON.", path) + } + return &c, nil +} + +func setParseDefault(e *env, newDefault, defaultApp string, parseConfig *parseConfig) error { + apps := parseConfig.Applications + if _, ok := apps[newDefault]; !ok { + parseConfig.pprintApps(e) + return stackerr.Newf("Invalid application name \"%s\". Please select from the valid applications printed above.", newDefault) } - return stackerr.Newf("Unknown project type: %s.", projectType) + + if defaultApp == "" { + apps[defaultKey] = &parseAppConfig{Link: newDefault} + } else { + apps[defaultKey].Link = newDefault + } + if err := storeConfig(e, parseConfig); err != nil { + return err + } + fmt.Fprintf(e.Out, "Default app set to %s.\n", newDefault) + return nil } diff --git a/parse_symbol_uploader.go b/parse_symbol_uploader.go index 445575d..bfdfd0c 100644 --- a/parse_symbol_uploader.go +++ b/parse_symbol_uploader.go @@ -69,7 +69,7 @@ func uploadSymbolFiles(files []string, commonHeaders map[string]string, removeFi } req.Header.Add("Content-Type", mimeType) res := make(map[string]interface{}) - if _, err := e.Client.Do(req, nil, &res); err != nil { + if _, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { wg.Error(err) return } diff --git a/parse_symbol_uploader_test.go b/parse_symbol_uploader_test.go index 59117f0..d353cc4 100644 --- a/parse_symbol_uploader_test.go +++ b/parse_symbol_uploader_test.go @@ -100,7 +100,7 @@ func TestUploadFiles(t *testing.T) { }, nil } }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} var filenames []string for _, name := range names { diff --git a/releases_cmd.go b/releases_cmd.go index 5b02cb2..2485c5c 100644 --- a/releases_cmd.go +++ b/releases_cmd.go @@ -74,12 +74,12 @@ Note that you can list files for all releases shown in "parse releases"`, return nil } -func (r *releasesCmd) run(e *env, c *client) error { +func (r *releasesCmd) run(e *env, c *context) error { u := &url.URL{ Path: "releases", } var releasesList []releasesResponse - if _, err := e.Client.Get(u, &releasesList); err != nil { + if _, err := e.ParseAPIClient.Get(u, &releasesList); err != nil { return stackerr.Wrap(err) } diff --git a/releases_cmd_test.go b/releases_cmd_test.go index f67c442..b7f0c72 100644 --- a/releases_cmd_test.go +++ b/releases_cmd_test.go @@ -33,9 +33,9 @@ func TestReleasesCmd(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - ensure.Nil(t, r.run(h.env, &client{})) + ensure.Nil(t, r.run(h.env, &context{})) expected := `Name Description Date v1 version 1 time 1 @@ -50,9 +50,9 @@ func TestReleasesCmdError(t *testing.T) { ht := transportFunc(func(r *http.Request) (*http.Response, error) { return nil, stackerr.New("Throws error") }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - ensure.NotNil(t, c.run(h.env, &client{})) + ensure.NotNil(t, c.run(h.env, &context{})) } func TestReleasesCmdPrintVersion(t *testing.T) { diff --git a/rollback_cmd.go b/rollback_cmd.go index 2abbb41..2c0eabb 100644 --- a/rollback_cmd.go +++ b/rollback_cmd.go @@ -16,7 +16,7 @@ type rollbackInfo struct { ReleaseName string `json:"releaseName,omitEmpty"` } -func (r *rollbackCmd) run(e *env, c *client) error { +func (r *rollbackCmd) run(e *env, c *context) error { var req rollbackInfo message := "previous release" if r.ReleaseName != "" { @@ -27,7 +27,7 @@ func (r *rollbackCmd) run(e *env, c *client) error { fmt.Fprintf(e.Out, "Rolling back to %s\n", message) var response rollbackInfo - if _, err := e.Client.Post(&url.URL{Path: "deploy"}, + if _, err := e.ParseAPIClient.Post(&url.URL{Path: "deploy"}, &req, &response); err != nil { return stackerr.Newf("Rollback failed with %s", stackerr.Wrap(err)) } diff --git a/rollback_cmd_test.go b/rollback_cmd_test.go index 6594a15..766d562 100644 --- a/rollback_cmd_test.go +++ b/rollback_cmd_test.go @@ -28,7 +28,7 @@ func newRollbackCmdHarness(t testing.TB) *Harness { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} return h } @@ -70,6 +70,6 @@ func TestRollbackError(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} ensure.Err(t, r.run(h.env, nil), regexp.MustCompile("something is wrong")) } diff --git a/run_helpers.go b/run_helpers.go new file mode 100644 index 0000000..4f3ed5c --- /dev/null +++ b/run_helpers.go @@ -0,0 +1,115 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +type cobraRun func(cmd *cobra.Command, args []string) + +// runNoArgs wraps a run function that shouldn't get any arguments. +func runNoArgs(e *env, f func(*env) error) cobraRun { + return func(cmd *cobra.Command, args []string) { + if len(args) != 0 { + fmt.Fprintf(e.Err, "unexpected arguments:%+v\n\n", args) + cmd.Help() + e.Exit(1) + } + if err := f(e); err != nil { + fmt.Fprintln(e.Err, errorString(e, err)) + e.Exit(1) + } + } +} + +// runWithArgs wraps a run function that can access arguments to cobraCmd +func runWithArgs(e *env, f func(*env, []string) error) cobraRun { + return func(cmd *cobra.Command, args []string) { + if err := f(e, args); err != nil { + fmt.Fprintln(e.Err, errorString(e, err)) + e.Exit(1) + } + } +} + +// runWithClient wraps a run function that should get an app, when the default is +// picked from the config in the current working directory. +func runWithClient(e *env, f func(*env, *context) error) cobraRun { + return func(cmd *cobra.Command, args []string) { + app := defaultKey + if len(args) > 1 { + fmt.Fprintf( + e.Err, + "unexpected arguments, only an optional app name is expected:%+v\n\n", + args, + ) + cmd.Help() + e.Exit(1) + } + if len(args) == 1 { + app = args[0] + } + cl, err := newContext(e, app) + if err != nil { + fmt.Fprintln(e.Err, errorString(e, err)) + e.Exit(1) + } + if err := f(e, cl); err != nil { + fmt.Fprintln(e.Err, errorString(e, err)) + e.Exit(1) + } + } +} + +// runWithAppClient wraps a run function that should get an app +func runWithAppClient(e *env, f func(*env, *context) error) cobraRun { + return func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Fprintf(e.Err, "please provide an app name\n\n") + cmd.Help() + e.Exit(1) + } + if len(args) > 1 { + fmt.Fprintf( + e.Err, + "unexpected arguments, only an app name is expected:%+v\n\n", + args, + ) + cmd.Help() + e.Exit(1) + } + app := args[0] + cl, err := newContext(e, app) + if err != nil { + fmt.Fprintln(e.Err, errorString(e, err)) + e.Exit(1) + } + if err := f(e, cl); err != nil { + fmt.Fprintln(e.Err, errorString(e, err)) + e.Exit(1) + } + } +} + +// runWithArgsClient wraps a run function that should get an app, whee the default is +// picked from the config in the current working directory. It also passes args to the +// runner function +func runWithArgsClient(e *env, f func(*env, *context, []string) error) cobraRun { + return func(cmd *cobra.Command, args []string) { + app := defaultKey + if len(args) > 1 { + app = args[0] + args = args[1:] + } + cl, err := newContext(e, app) + if err != nil { + fmt.Fprintln(e.Err, errorString(e, err)) + e.Exit(1) + } + if err := f(e, cl, args); err != nil { + fmt.Fprintln(e.Err, errorString(e, err)) + e.Exit(1) + } + } +} diff --git a/symbols_cmd.go b/symbols_cmd.go index 9a92956..f5dd469 100644 --- a/symbols_cmd.go +++ b/symbols_cmd.go @@ -15,7 +15,7 @@ type symbolsCmd struct { skipOsCheck bool } -func (s *symbolsCmd) run(e *env, c *client) error { +func (s *symbolsCmd) run(e *env, c *context) error { android := &androidSymbolUploader{ Path: s.path, Apk: s.apk, diff --git a/triggers_cmd.go b/triggers_cmd.go index 1526c30..86942cd 100644 --- a/triggers_cmd.go +++ b/triggers_cmd.go @@ -62,7 +62,7 @@ func readTriggerParams(e *env) (*triggerHook, error) { const defaultTriggersURL = "/1/hooks/triggers" -func (h *triggerHooksCmd) triggerHooksCreate(e *env, client *client) error { +func (h *triggerHooksCmd) triggerHooksCreate(e *env, ctx *context) error { params, err := readTriggerParams(e) if err != nil { return err @@ -72,7 +72,7 @@ func (h *triggerHooksCmd) triggerHooksCreate(e *env, client *client) error { if err != nil { return err } - _, err = e.Client.Post(triggersURL, params, &res) + _, err = e.ParseAPIClient.Post(triggersURL, params, &res) if err != nil { return err } @@ -88,7 +88,7 @@ func (h *triggerHooksCmd) triggerHooksCreate(e *env, client *client) error { return nil } -func (h *triggerHooksCmd) triggerHooksRead(e *env, client *client) error { +func (h *triggerHooksCmd) triggerHooksRead(e *env, ctx *context) error { u := defaultTriggersURL var trigger *triggerHook if !h.All { @@ -106,7 +106,7 @@ func (h *triggerHooksCmd) triggerHooksRead(e *env, client *client) error { var res struct { Results []*triggerHook `json:"results,omitempty"` } - _, err = e.Client.Get(triggersURL, &res) + _, err = e.ParseAPIClient.Get(triggersURL, &res) if err != nil { return err } @@ -130,7 +130,7 @@ func (h *triggerHooksCmd) triggerHooksRead(e *env, client *client) error { return nil } -func (h *triggerHooksCmd) triggerHooksUpdate(e *env, client *client) error { +func (h *triggerHooksCmd) triggerHooksUpdate(e *env, ctx *context) error { params, err := readTriggerParams(e) if err != nil { return err @@ -140,7 +140,7 @@ func (h *triggerHooksCmd) triggerHooksUpdate(e *env, client *client) error { if err != nil { return err } - _, err = e.Client.Put(triggersURL, &triggerHook{URL: params.URL}, &res) + _, err = e.ParseAPIClient.Put(triggersURL, &triggerHook{URL: params.URL}, &res) if err != nil { return err } @@ -157,7 +157,7 @@ func (h *triggerHooksCmd) triggerHooksUpdate(e *env, client *client) error { return nil } -func (h *triggerHooksCmd) triggerHooksDelete(e *env, client *client) error { +func (h *triggerHooksCmd) triggerHooksDelete(e *env, ctx *context) error { params, err := readTriggerName(e) if err != nil { return err @@ -174,7 +174,7 @@ func (h *triggerHooksCmd) triggerHooksDelete(e *env, client *client) error { var res triggerHook if getConfirmation(confirmMessage, e) { - _, err = e.Client.Put(triggersURL, map[string]interface{}{"__op": "Delete"}, &res) + _, err = e.ParseAPIClient.Put(triggersURL, map[string]interface{}{"__op": "Delete"}, &res) if err != nil { return err } @@ -192,7 +192,7 @@ func (h *triggerHooksCmd) triggerHooksDelete(e *env, client *client) error { return nil } -func (h *triggerHooksCmd) triggerHooks(e *env, c *client) error { +func (h *triggerHooksCmd) triggerHooks(e *env, c *context) error { hp := *h hp.All = true return hp.triggerHooksRead(e, c) diff --git a/triggers_cmd_test.go b/triggers_cmd_test.go index c8abc40..c06bc7b 100644 --- a/triggers_cmd_test.go +++ b/triggers_cmd_test.go @@ -100,7 +100,7 @@ func newTriggersHarness(t testing.TB) *Harness { Body: ioutil.NopCloser(jsonpipe.Encode(body)), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} return h } diff --git a/update_cmd.go b/update_cmd.go index efce3d5..4bcd2f9 100644 --- a/update_cmd.go +++ b/update_cmd.go @@ -34,7 +34,7 @@ func (u *updateCmd) latestVersion(e *env) (string, error) { Version string `json:"version"` } - if _, err := e.Client.Do(req, nil, &res); err != nil { + if _, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { return "", stackerr.Wrap(err) } diff --git a/update_cmd_test.go b/update_cmd_test.go index 77247a0..bbea48a 100644 --- a/update_cmd_test.go +++ b/update_cmd_test.go @@ -27,7 +27,7 @@ func TestLatestVersion(t *testing.T) { ), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} u := new(updateCmd) latestVersion, err := u.latestVersion(h.env) diff --git a/utils.go b/utils.go index 15028e6..e2915bb 100644 --- a/utils.go +++ b/utils.go @@ -9,6 +9,8 @@ import ( "strconv" "strings" + "github.com/facebookgo/errgroup" + "github.com/facebookgo/parse" "github.com/facebookgo/stackerr" ) @@ -118,8 +120,54 @@ func checkIfSupported(e *env, version string) (string, error) { Warning string `json:"warning"` } - if _, err := e.Client.Do(req, nil, &res); err != nil { + if _, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { return "", stackerr.Wrap(err) } return res.Warning, nil } + +// errorString returns the error string with our without the stack trace +// depending on the environment variable. this exists because we want plain +// messages for end users, but when we're working on the CLI we want the stack +// trace for debugging. +func errorString(e *env, err error) string { + type hasUnderlying interface { + HasUnderlying() error + } + + parseErr := func(err error) error { + if apiErr, ok := err.(*parse.Error); ok { + return errors.New(apiErr.Message) + } + return err + } + + lastErr := func(err error) error { + if serr, ok := err.(*stackerr.Error); ok { + if errs := stackerr.Underlying(serr); len(errs) != 0 { + err = errs[len(errs)-1] + } + } else { + if eu, ok := err.(hasUnderlying); ok { + err = eu.HasUnderlying() + } + } + + return parseErr(err) + } + + if !e.ErrorStack { + if merr, ok := err.(errgroup.MultiError); ok { + var multiError []error + for _, ierr := range []error(merr) { + multiError = append(multiError, lastErr(ierr)) + } + err = errgroup.MultiError(multiError) + } else { + err = lastErr(err) + } + return parseErr(err).Error() + } + + return err.Error() +} diff --git a/utils_test.go b/utils_test.go index af389b1..84b3e96 100644 --- a/utils_test.go +++ b/utils_test.go @@ -105,7 +105,7 @@ func TestIsSupportedWarning(t *testing.T) { ), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} message, err := checkIfSupported(h.env, "2.0.2") ensure.Nil(t, err) ensure.DeepEqual(t, message, "please update") @@ -128,7 +128,7 @@ func TestIsSupportedError(t *testing.T) { ), }, nil }) - h.env.Client = &Client{client: &parse.Client{Transport: ht}} + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} _, err := checkIfSupported(h.env, "2.0.2") ensure.Err(t, err, regexp.MustCompile("not supported")) } diff --git a/version_cmd.go b/version_cmd.go new file mode 100644 index 0000000..bbc335b --- /dev/null +++ b/version_cmd.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +type versionCmd struct{} + +func (c *versionCmd) run(e *env) error { + fmt.Fprintln(e.Out, version) + return nil +} + +func newVersionCmd(e *env) *cobra.Command { + var c versionCmd + cmd := &cobra.Command{ + Use: "version", + Short: "Gets the Command Line Tools version", + Long: `Gets the Command Line Tools version.`, + Run: runNoArgs(e, c.run), + Aliases: []string{"cliversion"}, + } + return cmd +} From 1773b63f5e14040fdc0cb52cddef044b56d3eda6 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 20 Aug 2015 02:47:53 +0000 Subject: [PATCH 017/106] remove git utils: since not needed in the diff --- git_utils.go | 129 --------------------------------------------------- 1 file changed, 129 deletions(-) delete mode 100644 git_utils.go diff --git a/git_utils.go b/git_utils.go deleted file mode 100644 index 87e10aa..0000000 --- a/git_utils.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/facebookgo/stackerr" -) - -type gitInfo struct { - repo string - authToken string - remote string - description string -} - -func (g *gitInfo) checkAbort(e *env) error { - _, err := exec.LookPath("git") - if err != nil { - return stackerr.New(`Unable to locate "git". -Please install "git" and ensure that you are able to run "git help" from the command prompt.`, - ) - } - - _, err = os.Lstat(filepath.Join(e.Root, ".git")) - if os.IsNotExist(err) { - return stackerr.Newf("%s is not a git repository", e.Root) - } - return nil -} - -func (g *gitInfo) runCmd(cmd *exec.Cmd, msg string) error { - err := cmd.Run() - if err == nil { - return nil - } - if _, ok := err.(*exec.ExitError); ok { - if msg != "" { - return stackerr.New(msg) - } - return stackerr.Newf("error executing: %q", strings.Join(cmd.Args, " ")) - } - return stackerr.Wrap(err) -} - -func (g *gitInfo) getDescription() string { - if g.description != "" { - return g.description - } - return fmt.Sprintf("committed by parse-cli at: %s", time.Now()) -} - -func (g *gitInfo) isDirty(e *env) (bool, error) { - if err := g.checkAbort(e); err != nil { - return false, err - } - - cmd := exec.Command("git", "status", "--porcelain") - var cmdOut, cmdErr bytes.Buffer - cmd.Stdout = &cmdOut - cmd.Stderr = &cmdErr - err := g.runCmd(cmd, fmt.Sprintf("Unable to determine the status of git repo: %s", g.repo)) - if err != nil { - fmt.Fprintln(e.Err, cmdErr.String()) - return false, err - } - - if cmdOut.Len() == 0 { - return false, nil - } - - fmt.Fprintf(e.Out, "Status of git repo: %q:\n%s", g.repo, cmdOut.String()) - return true, nil -} - -func (g *gitInfo) commit(e *env) error { - if err := g.checkAbort(e); err != nil { - return err - } - - cmd := exec.Command("git", "add", "-A", ".") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) - if err != nil { - return err - } - - cmd = exec.Command("git", "commit", "-a", "-m", g.getDescription()) - return g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) -} - -func (g *gitInfo) push(e *env, force bool) error { - if err := g.checkAbort(e); err != nil { - return err - } - command := []string{"push"} - if force { - command = append(command, "-f") - } - command = append(command, - fmt.Sprintf("https://:%s@%s", g.authToken, g.remote), - "master", - ) - cmd := exec.Command("git", command...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return g.runCmd( - cmd, - fmt.Sprintf("Unable to push to git remote: %s", g.remote), - ) -} - -func (g *gitInfo) clone(gitURL, path string) error { - _, err := exec.LookPath("git") - if err != nil { - return stackerr.New(`Unable to locate "git". -Please install "git" and ensure that you are able to run "git help" from the command prompt.`, - ) - } - - cmd := exec.Command("git", "clone", gitURL, path) - return g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) -} From 48a4ee6b190b14fede9d26c0638dac7840a01787 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 20 Aug 2015 16:06:21 +0000 Subject: [PATCH 018/106] update readme & version of parse-cli, fix help message --- README.md | 10 ++++++++-- configure_cmd.go | 2 +- main.go | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7cebb3b..c9b85aa 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,21 @@ It can be used to create new Parse apps, deploy Cloud Code to an app, view all r You can install `Parse Command Line Tool` by following instructions at [parse-cli](https://parse.com/apps/quickstart#cloud_code/). -Alternatively, you can just type the following command (assuming you already have Go installed). +Alternatively, you can just type the following command. + +NOTE: You should already have [Go](https://golang.org/doc/install) installed and GOPATH, GOROOT set to appropriate values. + ```bash - go get github.com/ParsePlatform/parse-cli + go get -t github.com/ParsePlatform/parse-cli ``` + This installs a binary called `parse-cli` at `$GOPATH/bin`. +Further, you can find the code for Parse CLI at: `$GOPATH/src/github.com/ParsePlatform/parse-cli`. The following commands are currently available in the `Parse Command Line Tool`: ```bash add Adds a new Parse app to config in current Cloud Code directory + configure Configure various Parse settings default Sets or gets the default Parse app deploy Deploys a Parse app develop Monitors for changes to code and deploys, also tails parse logs diff --git a/configure_cmd.go b/configure_cmd.go index 7ee528f..35d0497 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -52,7 +52,7 @@ func newConfigureCmd(e *env) *cobra.Command { cmd := &cobra.Command{ Use: "configure", - Short: "Configure various Parse settings.", + Short: "Configure various Parse settings", Long: "Configure various Parse settings like access tokens, project type, and more.", Run: func(c *cobra.Command, args []string) { c.Help() diff --git a/main.go b/main.go index ea5e4c8..56b6da5 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - version = "2.2.1" + version = "2.2.2" cloudDir = "cloud" hostingDir = "public" defaultBaseURL = "https://api.parse.com/1/" From 3988719bb139f528fbdea648de6428dbfee9c62a Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 21 Aug 2015 16:54:07 +0000 Subject: [PATCH 019/106] update account key creation flow --- configure_cmd.go | 36 +++++++++++++----------------- configure_cmd_test.go | 24 +++++++++----------- login.go | 52 +++++++++++++++++++++++-------------------- new.go | 18 ++------------- 4 files changed, 56 insertions(+), 74 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index 35d0497..e0aca63 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -11,26 +11,19 @@ type configureCmd struct { login login } -func (c *configureCmd) accessToken(e *env) error { - fmt.Fprintf(e.Out, - `Please enter an access token if you already generated it. - -If you do not have an access token or would like to generate a new one, -please type: "y" to open the browser or "n" to continue: `, - ) - - c.login.helpCreateToken(e) - - var credentials credentials - fmt.Fprintf(e.Out, "Access Token: ") - fmt.Fscanf(e.In, "%s\n", &credentials.token) +func (c *configureCmd) accountKey(e *env) error { + token, err := c.login.helpCreateToken(e) + if err != nil { + return err + } - _, err := (&apps{login: login{credentials: credentials}}).restFetchApps(e) + credentials := credentials{token: token} + _, err = (&apps{login: login{credentials: credentials}}).restFetchApps(e) if err != nil { if err == errAuth { fmt.Fprintf(e.Err, - `Sorry, the access token you provided is not valid. -Please follow instructions at %s to generate a new access token. + `Sorry, the account key you provided is not valid. +Please follow instructions at %s to generate a new account key. `, keysURL, ) @@ -53,16 +46,17 @@ func newConfigureCmd(e *env) *cobra.Command { cmd := &cobra.Command{ Use: "configure", Short: "Configure various Parse settings", - Long: "Configure various Parse settings like access tokens, project type, and more.", + Long: "Configure various Parse settings like account keys, project type, and more.", Run: func(c *cobra.Command, args []string) { c.Help() }, } cmd.AddCommand(&cobra.Command{ - Use: "token", - Short: "Store Parse access token on machine", - Long: "Stores Parse access token in ~/.parse/netrc.", - Run: runNoArgs(e, c.accessToken), + Use: "accountkey", + Short: "Store Parse account key on machine", + Long: "Stores Parse account key in ~/.parse/netrc.", + Run: runNoArgs(e, c.accountKey), + Aliases: []string{"key"}, }) return cmd } diff --git a/configure_cmd_test.go b/configure_cmd_test.go index 3d71085..e146525 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -16,23 +16,21 @@ func TestConfigureAcessToken(t *testing.T) { defer h.Stop() c := configureCmd{login: login{tokenReader: strings.NewReader("")}} - h.env.In = ioutil.NopCloser(strings.NewReader("n\ntoken\n")) - ensure.Nil(t, c.accessToken(h.env)) - ensure.DeepEqual(t, + h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.env)) + ensure.DeepEqual( + t, h.Out.String(), - `Please enter an access token if you already generated it. - -If you do not have an access token or would like to generate a new one, -please type: "y" to open the browser or "n" to continue: Please open "https://www.parse.com/account_keys" in the browser -and follow instructions to create an access token. -Access Token: Successfully stored credentials. + ` +Input your account key or press enter to generate a new one. +Account Key: Successfully stored credentials. `) - h.env.In = ioutil.NopCloser(strings.NewReader("n\nemail\ninvalid\n")) - ensure.Err(t, c.accessToken(h.env), regexp.MustCompile("Please try again")) + h.env.In = ioutil.NopCloser(strings.NewReader("email\ninvalid\n")) + ensure.Err(t, c.accountKey(h.env), regexp.MustCompile("Please try again")) ensure.DeepEqual(t, h.Err.String(), - `Sorry, the access token you provided is not valid. -Please follow instructions at https://www.parse.com/account_keys to generate a new access token. + `Sorry, the account key you provided is not valid. +Please follow instructions at https://www.parse.com/account_keys to generate a new account key. `, ) } diff --git a/login.go b/login.go index 448c2d3..8265fd1 100644 --- a/login.go +++ b/login.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "strings" "github.com/bgentry/go-netrc/netrc" "github.com/bgentry/speakeasy" @@ -168,8 +169,7 @@ func (l *login) authUserWithToken(e *env) error { _, err = apps.restFetchApps(e) if err == errAuth { fmt.Fprintln(e.Err, - `Sorry, the token you configured is not valid. - + `Sorry, the account key you configured is not valid. `) } if err != nil { @@ -188,8 +188,11 @@ func (l *login) authUser(e *env) error { apps := &apps{} fmt.Fprintln( e.Out, - `To avoid typing the email and password everytime, -please type "parse configure token" and provide a valid access token. + ` +We’ve changed the way the CLI works. +To save time logging in, you should create an account key. + +Type “parse configure accountkey” to create a new account key. Please log in to Parse using your email and password.`, ) @@ -216,26 +219,27 @@ Please log in to Parse using your email and password.`, return errAuth } -func (l *login) helpCreateToken(e *env) { - var shouldOpen string - fmt.Fscanf(e.In, "%s\n", &shouldOpen) - if shouldOpen == "n" { - fmt.Fprintf(e.Out, - `Please open %q in the browser -and follow instructions to create an access token. -`, - keysURL, - ) - return - } - err := open.Run(keysURL) - if err != nil { - fmt.Fprintf(e.Err, - `Sorry, we could not open %q in the browser. -Please open %q in the browser to create a new account key. +func (l *login) helpCreateToken(e *env) (string, error) { + for i := 0; i < 4; i++ { + fmt.Fprintln(e.Out, "\nInput your account key or press enter to generate a new one.") + fmt.Fprintf(e.Out, `Account Key: `) + + var token string + fmt.Fscanf(e.In, "%s\n", &token) + token = strings.TrimSpace(token) + if token != "" { + return token, nil + } + + err := open.Run(keysURL) + if err != nil { + fmt.Fprintf(e.Err, + `Sorry, we couldn’t open the browser for you. +Go here to generate an account key: %q `, - keysURL, - keysURL, - ) + keysURL, + ) + } } + return "", stackerr.New("Account key cannot be empty. Please try again.") } diff --git a/new.go b/new.go index fd56a2a..d97e865 100644 --- a/new.go +++ b/new.go @@ -187,24 +187,10 @@ func (n *newCmd) configureSample( return err } - // at this point user has already set a default app for the project - // use its properties to fetch latest jssdk version and set it in config - config, err := configFromDir(e.Root) - if err != nil { - return err - } - defaultApp, err := config.app(config.getDefaultApp()) - if err != nil { - return err - } - masterKey, err := defaultApp.getMasterKey(e) - if err != nil { - return err - } e.ParseAPIClient = e.ParseAPIClient.WithCredentials( parse.MasterKey{ - ApplicationID: defaultApp.getApplicationID(), - MasterKey: masterKey, + ApplicationID: app.ApplicationID, + MasterKey: app.MasterKey, }, ) From a94b0d84dff3acced2c8ddb485e11938fc585f59 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 23 Aug 2015 21:47:08 +0000 Subject: [PATCH 020/106] add timeout to get supported version --- utils.go | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/utils.go b/utils.go index e2915bb..0b0ed37 100644 --- a/utils.go +++ b/utils.go @@ -8,6 +8,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/facebookgo/errgroup" "github.com/facebookgo/parse" @@ -116,14 +117,27 @@ func checkIfSupported(e *env, version string) (string, error) { URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, } - var res struct { - Warning string `json:"warning"` + type result struct { + warning string + err error } - if _, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { - return "", stackerr.Wrap(err) + timeout := make(chan *result, 1) + go func() { + var res struct { + Warning string `json:"warning"` + } + _, err := e.ParseAPIClient.Do(req, nil, &res) + timeout <- &result{warning: res.Warning, err: err} + }() + + select { + case res := <-timeout: + return res.warning, stackerr.Wrap(res.err) + case <-time.After(time.Duration(500) * time.Millisecond): + return "", nil } - return res.Warning, nil + return "", nil } // errorString returns the error string with our without the stack trace From b3f0b8e76759e482a0e98288c90d4b83ef0b67dd Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 23 Aug 2015 22:45:50 +0000 Subject: [PATCH 021/106] bump version to 2.2.3 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 56b6da5..a3f15c0 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - version = "2.2.2" + version = "2.2.3" cloudDir = "cloud" hostingDir = "public" defaultBaseURL = "https://api.parse.com/1/" From 74eb70faa11a454b0ab6d86cefd0734dee133fc3 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 25 Aug 2015 01:49:18 +0000 Subject: [PATCH 022/106] add email support to selecting account keys --- config.go | 4 ++ configure_cmd.go | 74 ++++++++++++++++++----- configure_cmd_test.go | 38 ++++++++++-- list_cmd.go | 2 + login.go | 134 ++++++++++++++++++++++++++++++++---------- login_test.go | 81 +++++++++++++++++++++---- main.go | 22 ++++--- new.go | 9 +++ utils.go | 13 +++- utils_test.go | 27 ++++++++- 10 files changed, 331 insertions(+), 73 deletions(-) diff --git a/config.go b/config.go index 3797090..2cd0954 100644 --- a/config.go +++ b/config.go @@ -45,6 +45,10 @@ type projectConfig struct { // Currently jssdk version is the only // project level config for Parse type. Parse *parseProjectConfig `json:"parse,omitempty"` + // ParserEmail is an email id of the Parse developer. + // It is associated with this project. + // It is used to fetch appropriate credentials from netrc. + ParserEmail string `json:"email,omitempty"` } func getConfigFile(e *env) string { diff --git a/configure_cmd.go b/configure_cmd.go index e0aca63..720ac77 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -8,7 +8,8 @@ import ( ) type configureCmd struct { - login login + login login + isDefault bool } func (c *configureCmd) accountKey(e *env) error { @@ -17,29 +18,61 @@ func (c *configureCmd) accountKey(e *env) error { return err } - credentials := credentials{token: token} - _, err = (&apps{login: login{credentials: credentials}}).restFetchApps(e) + email, err := c.login.authToken(e, token) if err != nil { - if err == errAuth { - fmt.Fprintf(e.Err, - `Sorry, the account key you provided is not valid. -Please follow instructions at %s to generate a new account key. -`, - keysURL, + fmt.Fprintln(e.Err, "Could not store credentials. Please try again.\n") + return err + } + + if c.isDefault { + email = "" + } + + creds, _ := (&login{}).getTokenCredentials(e, email) + if creds != nil { + if c.isDefault { + fmt.Fprintln( + e.Err, + "Note: this operation will overwrite the default account key", ) } else { - fmt.Fprintf(e.Err, "Unable to validate token with error:\n%s\n", err) + fmt.Fprintf( + e.Err, + `Note: this operation will overwrite the account key: + %q +for email: %q +`, + last4(token), + email, + ) } - return stackerr.New("Could not store credentials. Please try again.") } - err = c.login.storeCredentials(e, &credentials) + err = c.login.storeCredentials(e, email, &credentials{token: token}) if err == nil { fmt.Fprintln(e.Out, "Successfully stored credentials.") } return stackerr.Wrap(err) } +func (c *configureCmd) parserEmail(e *env, args []string) error { + config, err := configFromDir(e.Root) + if err != nil { + return err + } + if len(args) != 1 { + return fmt.Errorf("Invalid args: %v, only an email argument is expected.", args) + } + config.getProjectConfig().ParserEmail = args[0] + err = storeProjectConfig(e, config) + if err != nil { + fmt.Fprintln(e.Err, "Could not set parser email for project.") + return err + } + fmt.Fprintf(e.Out, "Successfully configured email for current project to: %q\n", args[0]) + return nil +} + func newConfigureCmd(e *env) *cobra.Command { var c configureCmd @@ -51,12 +84,25 @@ func newConfigureCmd(e *env) *cobra.Command { c.Help() }, } - cmd.AddCommand(&cobra.Command{ + + keyCmd := &cobra.Command{ Use: "accountkey", Short: "Store Parse account key on machine", Long: "Stores Parse account key in ~/.parse/netrc.", Run: runNoArgs(e, c.accountKey), Aliases: []string{"key"}, - }) + } + keyCmd.Flags().BoolVarP(&c.isDefault, "default", "d", c.isDefault, + "Make this token a system default") + cmd.AddCommand(keyCmd) + + emailCmd := &cobra.Command{ + Use: "email", + Short: "Configures the parser email for this project", + Long: "Configures the parser email for current project.", + Run: runWithArgs(e, c.parserEmail), + } + cmd.AddCommand(emailCmd) + return cmd } diff --git a/configure_cmd_test.go b/configure_cmd_test.go index e146525..fc21c63 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -2,6 +2,7 @@ package main import ( "io/ioutil" + "path/filepath" "regexp" "strings" "testing" @@ -9,10 +10,10 @@ import ( "github.com/facebookgo/ensure" ) -func TestConfigureAcessToken(t *testing.T) { +func TestConfigureAccessToken(t *testing.T) { t.Parallel() - h, _ := newAppHarness(t) + h := newTokenHarness(t) defer h.Stop() c := configureCmd{login: login{tokenReader: strings.NewReader("")}} @@ -26,11 +27,38 @@ Input your account key or press enter to generate a new one. Account Key: Successfully stored credentials. `) h.env.In = ioutil.NopCloser(strings.NewReader("email\ninvalid\n")) - ensure.Err(t, c.accountKey(h.env), regexp.MustCompile("Please try again")) + ensure.Err(t, c.accountKey(h.env), regexp.MustCompile("is not valid")) ensure.DeepEqual(t, h.Err.String(), - `Sorry, the account key you provided is not valid. -Please follow instructions at https://www.parse.com/account_keys to generate a new account key. + "Could not store credentials. Please try again.\n\n", + ) +} + +func TestParserEmail(t *testing.T) { + t.Parallel() + + h := newTokenHarness(t) + h.makeEmptyRoot() + defer h.Stop() + + var n newCmd + ensure.Nil(t, n.createConfigWithContent(filepath.Join(h.env.Root, parseLocal), "{}")) + ensure.Nil(t, + n.createConfigWithContent( + filepath.Join(h.env.Root, parseProject), + `{"project_type": 1}`, + ), + ) + + var c configureCmd + ensure.Nil(t, c.parserEmail(h.env, []string{"email2"})) + ensure.DeepEqual( + t, + h.Out.String(), + `Successfully configured email for current project to: "email2" `, ) + + ensure.Err(t, c.parserEmail(h.env, nil), regexp.MustCompile("Invalid args:")) + ensure.Err(t, c.parserEmail(h.env, []string{"a", "b"}), regexp.MustCompile("Invalid args:")) } diff --git a/list_cmd.go b/list_cmd.go index fc85c57..a297ab5 100644 --- a/list_cmd.go +++ b/list_cmd.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "github.com/facebookgo/stackerr" @@ -23,6 +24,7 @@ func (l *listCmd) printListOfApps(e *env) error { return stackerr.Wrap(err) } config.pprintApps(e) + fmt.Fprintln(e.Out, "") return nil } diff --git a/login.go b/login.go index 8265fd1..05aac7d 100644 --- a/login.go +++ b/login.go @@ -4,12 +4,17 @@ import ( "errors" "fmt" "io" + "io/ioutil" + "net/http" + "net/url" "os" "path/filepath" + "regexp" "strings" "github.com/bgentry/go-netrc/netrc" "github.com/bgentry/speakeasy" + "github.com/facebookgo/jsonpipe" "github.com/facebookgo/stackerr" "github.com/mitchellh/go-homedir" "github.com/skratchdot/open-golang/open" @@ -34,9 +39,17 @@ If you do not remember your password, please follow instructions at: https://www.parse.com/login to reset your password`) - parseNetrc = filepath.Join(".parse", "netrc") + tokenErrMsgf = `Sorry, the account key: %q you provided is not valid. +Please follow instructions at %q to generate a new one. +` + keyNotFound = regexp.MustCompile("Could not find access key") + parseNetrc = filepath.Join(".parse", "netrc") ) +func accessKeyNotFound(err error) bool { + return keyNotFound.MatchString(err.Error()) +} + func (l *login) populateCreds(e *env) error { if l.credentials.email != "" && l.credentials.password != "" { return nil @@ -82,7 +95,7 @@ func (l *login) getTokensReader() (io.Reader, error) { return file, nil } -func (l *login) getTokenCredentials(e *env) (*credentials, error) { +func (l *login) getTokenCredentials(e *env, email string) (*credentials, error) { reader, err := l.getTokensReader() if err != nil { return nil, stackerr.Wrap(err) @@ -91,22 +104,46 @@ func (l *login) getTokenCredentials(e *env) (*credentials, error) { if err != nil { return nil, stackerr.Wrap(err) } - server, err := getHostFromURL(e.Server) + server, err := getHostFromURL(e.Server, email) if err != nil { return nil, err } machine := tokens.FindMachine(server) - if machine == nil { - return nil, stackerr.Newf("could not find token for %s", server) + if machine != nil { + return &credentials{ + token: machine.Password, + }, nil + } + + if email == "" { + return nil, stackerr.Newf("Could not find access key for %q", server) + } + + // check for system default account key for the given server + // since we could not find account key for the given account (email) + server, err = getHostFromURL(e.Server, "") + if err != nil { + return nil, err } - return &credentials{ - token: machine.Password, - }, nil + machine = tokens.FindMachine(server) + if machine != nil { + return &credentials{ + token: machine.Password, + }, nil + } + return nil, stackerr.Newf( + `Could not find access key for email: %q, +and default access key not configured for %q +`, + email, + e.Server, + ) } func (l *login) updatedNetrcContent( e *env, content io.Reader, + email string, credentials *credentials, ) ([]byte, error) { tokens, err := netrc.Parse(content) @@ -114,7 +151,7 @@ func (l *login) updatedNetrcContent( return nil, stackerr.Wrap(err) } - server, err := getHostFromURL(e.Server) + server, err := getHostFromURL(e.Server, email) if err != nil { return nil, err } @@ -132,7 +169,7 @@ func (l *login) updatedNetrcContent( return updatedContent, nil } -func (l *login) storeCredentials(e *env, credentials *credentials) error { +func (l *login) storeCredentials(e *env, email string, credentials *credentials) error { if l.tokenReader != nil { // tests should not store credentials return nil @@ -142,6 +179,7 @@ func (l *login) storeCredentials(e *env, credentials *credentials) error { if err != nil { return stackerr.Wrap(err) } + location := filepath.Join(homeDir, parseNetrc) if err := os.MkdirAll(filepath.Dir(location), 0755); err != nil { return stackerr.Wrap(err) @@ -150,51 +188,85 @@ func (l *login) storeCredentials(e *env, credentials *credentials) error { if err != nil { return stackerr.Wrap(err) } - - content, err := l.updatedNetrcContent(e, file, credentials) + content, err := l.updatedNetrcContent(e, file, email, credentials) + if err != nil { + return err + } file, err = os.OpenFile(location, os.O_WRONLY|os.O_TRUNC, 0600) _, err = file.Write(content) return stackerr.Wrap(err) } -func (l *login) authUserWithToken(e *env) error { - tokenCredentials, err := l.getTokenCredentials(e) +func (l *login) authToken(e *env, token string) (string, error) { + req := &http.Request{ + Method: "POST", + URL: &url.URL{Path: "accountkey"}, + Body: ioutil.NopCloser( + jsonpipe.Encode( + map[string]string{ + "accountKey": token, + }, + ), + ), + } + + res := &struct { + Email string `json:"email"` + }{} + if response, err := e.ParseAPIClient.Do(req, nil, res); err != nil { + if response.StatusCode == http.StatusUnauthorized { + return "", stackerr.Newf(tokenErrMsgf, last4(token), keysURL) + } + return "", stackerr.Wrap(err) + } - if err != nil { - return err + if e.ParserEmail != "" && res.Email != e.ParserEmail { + return "", stackerr.Newf("Account key %q does not belong to %q", last4(token), e.ParserEmail) } + return res.Email, nil +} - apps := &apps{login: login{credentials: *tokenCredentials}} - _, err = apps.restFetchApps(e) - if err == errAuth { - fmt.Fprintln(e.Err, - `Sorry, the account key you configured is not valid. -`) +func (l *login) authUserWithToken(e *env) (string, error) { + tokenCredentials, err := l.getTokenCredentials(e, e.ParserEmail) + if err != nil { + if stackerr.HasUnderlying(err, stackerr.MatcherFunc(accessKeyNotFound)) { + fmt.Fprintln(e.Err, errorString(e, err)) + } + return "", err } + + email, err := l.authToken(e, tokenCredentials.token) if err != nil { - return stackerr.Wrap(err) + fmt.Fprintf(e.Err, "Account key could not be used.\nError: %s\n\n", errorString(e, err)) + return "", err } l.credentials = *tokenCredentials - return nil + return email, nil } func (l *login) authUser(e *env) error { - if l.authUserWithToken(e) == nil { + _, err := l.authUserWithToken(e) + if err == nil { return nil } + // user never created an account key: educate them + if stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) { + fmt.Fprintln( + e.Out, + `We've changed the way the CLI works. +To save time logging in, you should create an account key. +`) + } + apps := &apps{} fmt.Fprintln( e.Out, - ` -We’ve changed the way the CLI works. -To save time logging in, you should create an account key. - -Type “parse configure accountkey” to create a new account key. + `Type "parse configure accountkey" to create a new account key. -Please log in to Parse using your email and password.`, +Please login to Parse using your email and password.`, ) for i := 0; i < numRetries; i++ { err := l.populateCreds(e) diff --git a/login_test.go b/login_test.go index 5df1bc0..e693405 100644 --- a/login_test.go +++ b/login_test.go @@ -1,13 +1,44 @@ package main import ( + "encoding/json" + "io/ioutil" + "net/http" "regexp" "strings" "testing" "github.com/facebookgo/ensure" + "github.com/facebookgo/parse" ) +func newTokenHarness(t testing.TB) *Harness { + h := newHarness(t) + ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.URL.Path, "/1/accountkey") + ensure.DeepEqual(t, r.Method, "POST") + + key := &struct { + AccountKey string `json:"accountKey"` + }{} + ensure.Nil(t, json.NewDecoder(ioutil.NopCloser(r.Body)).Decode(key)) + + if key.AccountKey != "token" { + return &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect token"}`)), + }, nil + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(`{"email": "email"}`)), + }, nil + }) + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + return h +} + func TestPopulateCreds(t *testing.T) { t.Parallel() @@ -36,31 +67,56 @@ func TestGetTokenCredentials(t *testing.T) { password token `, ) - credentials, err := l.getTokenCredentials(h.env) + credentials, err := l.getTokenCredentials(h.env, "") ensure.Nil(t, err) ensure.DeepEqual(t, credentials.token, "token") h.env.Server = "http://api.parse.com" - credentials, err = l.getTokenCredentials(h.env) - ensure.Err(t, err, regexp.MustCompile("could not find token for")) + credentials, err = l.getTokenCredentials(h.env, "") + ensure.Err(t, err, keyNotFound) } func TestAuthUserWithToken(t *testing.T) { t.Parallel() - h, _ := newAppHarness(t) + h := newTokenHarness(t) defer h.Stop() l := &login{} + h.env.ParserEmail = "email" h.env.Server = "http://api.example.org/1/" l.tokenReader = strings.NewReader( - `machine api.example.org - login email + `machine api.example.org#email + login default password token `, ) - ensure.Nil(t, l.authUserWithToken(h.env)) + _, err := l.authUserWithToken(h.env) + ensure.Nil(t, err) + + h.env.ParserEmail = "email2" + + l.tokenReader = strings.NewReader( + `machine api.example.org#email2 + login default + password token + `, + ) + _, err = l.authUserWithToken(h.env) + ensure.Err(t, err, regexp.MustCompile(`does not belong to "email2"`)) + + h.env.ParserEmail = "" + + l.tokenReader = strings.NewReader( + `machine api.example.org + login default + password token2 + `, + ) + _, err = l.authUserWithToken(h.env) + ensure.Err(t, err, regexp.MustCompile("provided is not valid")) + } func TestUpdatedNetrcContent(t *testing.T) { @@ -72,9 +128,10 @@ func TestUpdatedNetrcContent(t *testing.T) { l := &login{} h.env.Server = "https://api.example.com/1/" - updated, err := l.updatedNetrcContent(h.env, + updated, err := l.updatedNetrcContent( + h.env, strings.NewReader( - `machine api.example.com + `machine api.example.com#email login default password token0 @@ -83,13 +140,14 @@ machine api.example.org password token `, ), + "email", &credentials{token: "token"}, ) ensure.Nil(t, err) ensure.DeepEqual(t, string(updated), - `machine api.example.com + `machine api.example.com#email login default password token @@ -107,6 +165,7 @@ machine api.example.org password token `, ), + "email", &credentials{token: "token"}, ) @@ -117,7 +176,7 @@ machine api.example.org login default password token -machine api.example.org +machine api.example.org#email login default password token`, ) diff --git a/main.go b/main.go index a3f15c0..a76126a 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ type env struct { Root string // project root Server string // parse api server Type int // project type + ParserEmail string // email associated with developer parse account ErrorStack bool Out io.Writer Err io.Writer @@ -50,14 +51,15 @@ func main() { }() e := env{ - Root: os.Getenv("PARSE_ROOT"), - Server: os.Getenv("PARSE_SERVER"), - ErrorStack: os.Getenv("PARSE_ERROR_STACK") == "1", - Out: os.Stdout, - Err: os.Stderr, - In: os.Stdin, - Exit: os.Exit, - Clock: clock.New(), + Root: os.Getenv("PARSE_ROOT"), + Server: os.Getenv("PARSE_SERVER"), + ErrorStack: os.Getenv("PARSE_ERROR_STACK") == "1", + ParserEmail: os.Getenv("PARSER_EMAIL"), + Out: os.Stdout, + Err: os.Stderr, + In: os.Stdin, + Exit: os.Exit, + Clock: clock.New(), } if e.Root == "" { cur, err := os.Getwd() @@ -74,6 +76,9 @@ func main() { os.Exit(1) } e.Type = config.getProjectConfig().Type + if e.ParserEmail == "" { + e.ParserEmail = config.getProjectConfig().ParserEmail + } } else { e.Type = legacyParseFormat e.Root = getLegacyProjectRoot(&e, cur) @@ -87,6 +92,7 @@ func main() { if e.Server == "" { e.Server = defaultBaseURL } + apiClient, err := newParseAPIClient(&e) if err != nil { fmt.Fprintln(e.Err, err) diff --git a/new.go b/new.go index d97e865..5357e5c 100644 --- a/new.go +++ b/new.go @@ -240,6 +240,15 @@ func (n *newCmd) run(e *env) error { if err := n.configureSample(addCmd, app, nil, e); err != nil { return err } + if token := apps.login.credentials.token; token != "" { + email, err := apps.login.authToken(e, token) + if err != nil { + return err + } + if err := (&configureCmd{}).parserEmail(e, []string{email}); err != nil { + return err + } + } fmt.Fprintf(e.Out, n.cloudCodeHelpMessage(e, app)) return nil diff --git a/utils.go b/utils.go index 0b0ed37..5b32f24 100644 --- a/utils.go +++ b/utils.go @@ -97,7 +97,7 @@ func validateURL(urlStr string) error { return nil } -func getHostFromURL(urlStr string) (string, error) { +func getHostFromURL(urlStr, email string) (string, error) { netURL, err := url.Parse(urlStr) if err != nil { return "", stackerr.Wrap(err) @@ -106,6 +106,9 @@ func getHostFromURL(urlStr string) (string, error) { if server == "" { return "", stackerr.Newf("%s is not a valid url", urlStr) } + if email != "" { + return fmt.Sprintf("%s#%s", server, email), nil + } return server, nil } @@ -140,6 +143,14 @@ func checkIfSupported(e *env, version string) (string, error) { return "", nil } +func last4(str string) string { + l := len(str) + if l > 4 { + return fmt.Sprintf("%s%s", strings.Repeat("*", l-4), str[l-4:l]) + } + return str +} + // errorString returns the error string with our without the stack trace // depending on the environment variable. this exists because we want plain // messages for end users, but when we're working on the CLI we want the stack diff --git a/utils_test.go b/utils_test.go index 84b3e96..f3bb9b1 100644 --- a/utils_test.go +++ b/utils_test.go @@ -74,17 +74,27 @@ func TestABWithNumbers(t *testing.T) { func TestGetHostFromURL(t *testing.T) { urlStr := "https://api.parse.com/1/" - host, err := getHostFromURL(urlStr) + host, err := getHostFromURL(urlStr, "") ensure.Nil(t, err) ensure.DeepEqual(t, host, "api.parse.com") + urlStr = "https://api.parse.com/1/" + host, err = getHostFromURL(urlStr, "yolo@earth.com") + ensure.Nil(t, err) + ensure.DeepEqual(t, host, "api.parse.com#yolo@earth.com") + urlStr = "https://api.example.com:8080/1/" - host, err = getHostFromURL(urlStr) + host, err = getHostFromURL(urlStr, "") ensure.Nil(t, err) ensure.DeepEqual(t, host, "api.example.com") + urlStr = "https://api.example.com:8080/1/" + host, err = getHostFromURL(urlStr, "yolo@earth.com") + ensure.Nil(t, err) + ensure.DeepEqual(t, host, "api.example.com#yolo@earth.com") + urlStr = "api.example.com:8080:90" - host, err = getHostFromURL(urlStr) + host, err = getHostFromURL(urlStr, "") ensure.Err(t, err, regexp.MustCompile("not a valid url")) } @@ -132,3 +142,14 @@ func TestIsSupportedError(t *testing.T) { _, err := checkIfSupported(h.env, "2.0.2") ensure.Err(t, err, regexp.MustCompile("not supported")) } + +func TestLastFour(t *testing.T) { + t.Parallel() + + ensure.DeepEqual(t, last4(""), "") + ensure.DeepEqual(t, last4("a"), "a") + ensure.DeepEqual(t, last4("ab"), "ab") + ensure.DeepEqual(t, last4("abc"), "abc") + ensure.DeepEqual(t, last4("abcd"), "abcd") + ensure.DeepEqual(t, last4("abcdefg"), "***defg") +} From e08f5b3de04727909e19b812f53dff56e0267d4b Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 26 Aug 2015 14:29:40 +0000 Subject: [PATCH 023/106] add email support for legacy config format --- config.go | 1 + legacy_config.go | 1 + 2 files changed, 2 insertions(+) diff --git a/config.go b/config.go index 2cd0954..c1ff322 100644 --- a/config.go +++ b/config.go @@ -172,6 +172,7 @@ func storeProjectConfig(e *env, c config) error { } lconf := &legacyConfig{Applications: p.Applications} lconf.Global.ParseVersion = p.projectConfig.Parse.JSSDK + lconf.Global.ParserEmail = p.projectConfig.ParserEmail return writeLegacyConfigFile( lconf, filepath.Join(e.Root, legacyConfigFile), diff --git a/legacy_config.go b/legacy_config.go index d025453..4f593f5 100644 --- a/legacy_config.go +++ b/legacy_config.go @@ -16,6 +16,7 @@ var legacyConfigFile = filepath.Join(configDir, "global.json") type legacyConfig struct { Global struct { ParseVersion string `json:"parseVersion,omitempty"` + ParserEmail string `json:"email,omitempty"` } `json:"global,omitempty"` Applications map[string]*parseAppConfig `json:"applications,omitempty"` } From 8bda36a7dbb7c47ef7f5d431ce1b9d2fe75b07e1 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 26 Aug 2015 16:11:18 +0000 Subject: [PATCH 024/106] add migrate command to upgrate to preferred config format --- commands.go | 1 + config.go | 7 ++-- migrate_cmd.go | 80 +++++++++++++++++++++++++++++++++++++++++++++ migrate_cmd_test.go | 78 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 migrate_cmd.go create mode 100644 migrate_cmd_test.go diff --git a/commands.go b/commands.go index ebf9081..3d4acff 100644 --- a/commands.go +++ b/commands.go @@ -34,6 +34,7 @@ http://parse.com`, c.AddCommand(newJsSdkCmd(e)) c.AddCommand(newListCmd(e)) c.AddCommand(newLogsCmd(e)) + c.AddCommand(newMigrateCmd(e)) c.AddCommand(newNewCmd(e)) c.AddCommand(newReleasesCmd(e)) c.AddCommand(newRollbackCmd(e)) diff --git a/config.go b/config.go index c1ff322..a598225 100644 --- a/config.go +++ b/config.go @@ -68,8 +68,11 @@ func configFromDir(dir string) (config, error) { } if l != nil { // legacy config format projectConfig := &projectConfig{ - Type: legacyParseFormat, - Parse: &parseProjectConfig{JSSDK: l.Global.ParseVersion}, + Type: legacyParseFormat, + Parse: &parseProjectConfig{ + JSSDK: l.Global.ParseVersion, + }, + ParserEmail: l.Global.ParserEmail, } applications := l.Applications if applications == nil { diff --git a/migrate_cmd.go b/migrate_cmd.go new file mode 100644 index 0000000..191dc3c --- /dev/null +++ b/migrate_cmd.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type migrateCmd struct { + retainMaster bool +} + +func (m *migrateCmd) upgradeLegacy(e *env, c config) (*parseConfig, error) { + p := c.getProjectConfig() + if p.Type != legacyParseFormat { + return nil, stackerr.New("Already using the preferred config format.") + } + p.Type = parseFormat + config, ok := (c).(*parseConfig) + if !ok { + return nil, stackerr.Newf("Unexpected config format: %d", p.Type) + } + if !m.retainMaster { + for _, app := range config.Applications { + app.MasterKey = "" + } + } + return config, nil +} + +func (m *migrateCmd) run(e *env) error { + c, err := configFromDir(e.Root) + if err != nil { + return err + } + + c, err = m.upgradeLegacy(e, c) + if err != nil { + return err + } + localErr := storeConfig(e, c) + projectErr := storeProjectConfig(e, c) + if localErr == nil && projectErr == nil { + legacy := filepath.Join(e.Root, legacyConfigFile) + err := os.Remove(legacy) + if err != nil { + fmt.Fprintf(e.Err, "Could not delete: %q. Please remove this file manually.\n", legacy) + } + } else { + local := filepath.Join(e.Root, parseLocal) + err := os.Remove(local) + if err != nil { + fmt.Fprintf(e.Err, "Failed to clean up: %q. Please remove this file manually.\n", local) + } + project := filepath.Join(e.Root, parseProject) + err = os.Remove(project) + if err != nil { + fmt.Fprintf(e.Err, "Failed to clean up: %q. Please remove this file manually.\n", project) + } + } + return nil +} + +func newMigrateCmd(e *env) *cobra.Command { + var m migrateCmd + cmd := &cobra.Command{ + Use: "migrate", + Short: "Migrate project config format to preferred format", + Long: `Use this on projects with legacy config format to migrate +to the preferred format. +`, + Run: runNoArgs(e, m.run), + } + cmd.Flags().BoolVarP(&m.retainMaster, "retain", "r", m.retainMaster, + "Retain any master keys present in config during migration") + return cmd +} diff --git a/migrate_cmd_test.go b/migrate_cmd_test.go new file mode 100644 index 0000000..26dda52 --- /dev/null +++ b/migrate_cmd_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "regexp" + "testing" + + "github.com/facebookgo/ensure" +) + +func TestUpgradeLegacyNoOp(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + var m migrateCmd + c := &parseConfig{projectConfig: &projectConfig{Type: parseFormat}} + _, err := m.upgradeLegacy(h.env, c) + ensure.Err(t, err, regexp.MustCompile("Already using the preferred config format.")) +} + +func TestUpgradeLegacyRetainMaster(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + m := migrateCmd{retainMaster: true} + c := &parseConfig{ + Applications: map[string]*parseAppConfig{ + "app": {ApplicationID: "a", MasterKey: "m"}, + }, + projectConfig: &projectConfig{Type: legacyParseFormat}, + } + config, err := m.upgradeLegacy(h.env, c) + ensure.Nil(t, err) + ensure.DeepEqual(t, config.Applications["app"].MasterKey, "m") +} + +func TestUpgradeLegacy(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + m := migrateCmd{retainMaster: false} + c := &parseConfig{ + Applications: map[string]*parseAppConfig{ + "app": {ApplicationID: "a", MasterKey: "m"}, + }, + projectConfig: &projectConfig{Type: legacyParseFormat}, + } + config, err := m.upgradeLegacy(h.env, c) + ensure.Nil(t, err) + ensure.DeepEqual(t, config.Applications["app"].MasterKey, "") +} + +func TestUpgradeLegacyWithEmail(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + m := migrateCmd{retainMaster: false} + c := &parseConfig{ + Applications: map[string]*parseAppConfig{ + "app": {ApplicationID: "a", MasterKey: "m"}, + }, + projectConfig: &projectConfig{ + Type: legacyParseFormat, + ParserEmail: "test@email.com", + }, + } + config, err := m.upgradeLegacy(h.env, c) + ensure.Nil(t, err) + ensure.DeepEqual(t, config.Applications["app"].MasterKey, "") + ensure.DeepEqual(t, config.getProjectConfig().ParserEmail, "test@email.com") +} From e7c34241c8dc3a514bbbe5db21bfda8cfdad3dc5 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 26 Aug 2015 22:41:25 +0000 Subject: [PATCH 025/106] messaging & some more commands --- configure_cmd.go | 5 ++++- configure_cmd_test.go | 2 +- login.go | 1 + migrate_cmd.go | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index 720ac77..7b3802c 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -50,7 +50,10 @@ for email: %q err = c.login.storeCredentials(e, email, &credentials{token: token}) if err == nil { - fmt.Fprintln(e.Out, "Successfully stored credentials.") + if c.isDefault { + fmt.Fprintln(e.Out, "Successfully stored default account key.") + } + fmt.Fprintf(e.Out, "Successfully stored account key for: %q.\n", email) } return stackerr.Wrap(err) } diff --git a/configure_cmd_test.go b/configure_cmd_test.go index fc21c63..e8d231e 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -24,7 +24,7 @@ func TestConfigureAccessToken(t *testing.T) { h.Out.String(), ` Input your account key or press enter to generate a new one. -Account Key: Successfully stored credentials. +Account Key: Successfully stored account key for: "email". `) h.env.In = ioutil.NopCloser(strings.NewReader("email\ninvalid\n")) ensure.Err(t, c.accountKey(h.env), regexp.MustCompile("is not valid")) diff --git a/login.go b/login.go index 05aac7d..038810e 100644 --- a/login.go +++ b/login.go @@ -265,6 +265,7 @@ To save time logging in, you should create an account key. fmt.Fprintln( e.Out, `Type "parse configure accountkey" to create a new account key. +Read more at: https://parse.com/docs/js/guide#command-line-account-keys Please login to Parse using your email and password.`, ) diff --git a/migrate_cmd.go b/migrate_cmd.go index 191dc3c..a322d99 100644 --- a/migrate_cmd.go +++ b/migrate_cmd.go @@ -61,6 +61,7 @@ func (m *migrateCmd) run(e *env) error { fmt.Fprintf(e.Err, "Failed to clean up: %q. Please remove this file manually.\n", project) } } + fmt.Fprintln(e.Out, "Successfully migrated to the preferred config format.") return nil } From 15008116708e5e853b4fb1f9dfe3493d11200807 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 27 Aug 2015 07:04:39 +0000 Subject: [PATCH 026/106] bump to version 2.2.4 --- login.go | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/login.go b/login.go index 038810e..d68b44f 100644 --- a/login.go +++ b/login.go @@ -20,7 +20,7 @@ import ( "github.com/skratchdot/open-golang/open" ) -const keysURL = "https://www.parse.com/account_keys" +const keysURL = "https://www.parse.com/account/keys" type credentials struct { email string diff --git a/main.go b/main.go index a76126a..9c6cb0e 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - version = "2.2.3" + version = "2.2.4" cloudDir = "cloud" hostingDir = "public" defaultBaseURL = "https://api.parse.com/1/" From 26c214527d2c5b2c44bc2300424e636077814154 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 27 Aug 2015 14:15:08 +0000 Subject: [PATCH 027/106] fix minor messaging bugs in configure token --- configure_cmd.go | 17 ++++++++---- configure_cmd_test.go | 61 +++++++++++++++++++++++++++++++++++++++-- login.go | 42 ++++++++++++++++------------- login_test.go | 63 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 155 insertions(+), 28 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index 7b3802c..ddb48c0 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -2,14 +2,16 @@ package main import ( "fmt" + "io" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) type configureCmd struct { - login login - isDefault bool + login login + isDefault bool + tokenReader io.Reader // for testing } func (c *configureCmd) accountKey(e *env) error { @@ -28,14 +30,18 @@ func (c *configureCmd) accountKey(e *env) error { email = "" } - creds, _ := (&login{}).getTokenCredentials(e, email) + var l login + if c.tokenReader != nil { + l.tokenReader = c.tokenReader + } + foundEmail, creds, _ := l.getTokenCredentials(e, email) if creds != nil { if c.isDefault { fmt.Fprintln( e.Err, "Note: this operation will overwrite the default account key", ) - } else { + } else if foundEmail { fmt.Fprintf( e.Err, `Note: this operation will overwrite the account key: @@ -52,8 +58,9 @@ for email: %q if err == nil { if c.isDefault { fmt.Fprintln(e.Out, "Successfully stored default account key.") + } else { + fmt.Fprintf(e.Out, "Successfully stored account key for: %q.\n", email) } - fmt.Fprintf(e.Out, "Successfully stored account key for: %q.\n", email) } return stackerr.Wrap(err) } diff --git a/configure_cmd_test.go b/configure_cmd_test.go index e8d231e..cca065f 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -10,13 +10,14 @@ import ( "github.com/facebookgo/ensure" ) -func TestConfigureAccessToken(t *testing.T) { +func TestConfigureAccountKey(t *testing.T) { t.Parallel() h := newTokenHarness(t) defer h.Stop() c := configureCmd{login: login{tokenReader: strings.NewReader("")}} + h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) ensure.Nil(t, c.accountKey(h.env)) ensure.DeepEqual( @@ -26,12 +27,68 @@ func TestConfigureAccessToken(t *testing.T) { Input your account key or press enter to generate a new one. Account Key: Successfully stored account key for: "email". `) - h.env.In = ioutil.NopCloser(strings.NewReader("email\ninvalid\n")) + + h.env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) ensure.Err(t, c.accountKey(h.env), regexp.MustCompile("is not valid")) ensure.DeepEqual(t, h.Err.String(), "Could not store credentials. Please try again.\n\n", ) + + h.env.Server = "http://api.parse.com/1/" + c.tokenReader = strings.NewReader( + `machine api.parse.com#email + login default + password token2 + `, + ) + h.Err.Reset() + h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.env)) + ensure.DeepEqual(t, h.Err.String(), + `Note: this operation will overwrite the account key: + "*oken" +for email: "email" +`) + + h.env.Server = "http://api.parse.com/1/" + c.tokenReader = strings.NewReader( + `machine api.parse.com#email + login default + password token2 + `, + ) + c.isDefault = true + h.Err.Reset() + h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.env)) + ensure.DeepEqual(t, h.Err.String(), "") + + h.env.Server = "http://api.parse.com/1/" + c.tokenReader = strings.NewReader( + `machine api.parse.com + login default + password token2 + `, + ) + c.isDefault = true + h.Err.Reset() + h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.env)) + ensure.DeepEqual(t, h.Err.String(), "Note: this operation will overwrite the default account key\n") + + h.env.Server = "http://api.parse.com/1/" + c.tokenReader = strings.NewReader( + `machine api.parse.com + login default + password token2 + `, + ) + h.Err.Reset() + c.isDefault = false + h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.env)) + ensure.DeepEqual(t, h.Err.String(), "") } func TestParserEmail(t *testing.T) { diff --git a/login.go b/login.go index d68b44f..fd4f8ca 100644 --- a/login.go +++ b/login.go @@ -42,7 +42,7 @@ to reset your password`) tokenErrMsgf = `Sorry, the account key: %q you provided is not valid. Please follow instructions at %q to generate a new one. ` - keyNotFound = regexp.MustCompile("Could not find access key") + keyNotFound = regexp.MustCompile("Could not find account key") parseNetrc = filepath.Join(".parse", "netrc") ) @@ -95,49 +95,53 @@ func (l *login) getTokensReader() (io.Reader, error) { return file, nil } -func (l *login) getTokenCredentials(e *env, email string) (*credentials, error) { +func (l *login) getTokenCredentials(e *env, email string) (bool, *credentials, error) { reader, err := l.getTokensReader() if err != nil { - return nil, stackerr.Wrap(err) + return false, nil, stackerr.Wrap(err) } tokens, err := netrc.Parse(reader) if err != nil { - return nil, stackerr.Wrap(err) + return false, nil, stackerr.Wrap(err) } server, err := getHostFromURL(e.Server, email) if err != nil { - return nil, err + return false, nil, err } machine := tokens.FindMachine(server) if machine != nil { - return &credentials{ - token: machine.Password, - }, nil + return true, + &credentials{ + token: machine.Password, + }, nil } if email == "" { - return nil, stackerr.Newf("Could not find access key for %q", server) + return false, nil, stackerr.Newf("Could not find account key for %q", server) } // check for system default account key for the given server // since we could not find account key for the given account (email) server, err = getHostFromURL(e.Server, "") if err != nil { - return nil, err + return false, nil, err } machine = tokens.FindMachine(server) if machine != nil { - return &credentials{ - token: machine.Password, - }, nil + return false, + &credentials{ + token: machine.Password, + }, nil } - return nil, stackerr.Newf( - `Could not find access key for email: %q, + return false, + nil, + stackerr.Newf( + `Could not find account key for email: %q, and default access key not configured for %q `, - email, - e.Server, - ) + email, + e.Server, + ) } func (l *login) updatedNetrcContent( @@ -228,7 +232,7 @@ func (l *login) authToken(e *env, token string) (string, error) { } func (l *login) authUserWithToken(e *env) (string, error) { - tokenCredentials, err := l.getTokenCredentials(e, e.ParserEmail) + _, tokenCredentials, err := l.getTokenCredentials(e, e.ParserEmail) if err != nil { if stackerr.HasUnderlying(err, stackerr.MatcherFunc(accessKeyNotFound)) { fmt.Fprintln(e.Err, errorString(e, err)) diff --git a/login_test.go b/login_test.go index e693405..3ae1379 100644 --- a/login_test.go +++ b/login_test.go @@ -67,13 +67,72 @@ func TestGetTokenCredentials(t *testing.T) { password token `, ) - credentials, err := l.getTokenCredentials(h.env, "") + _, credentials, err := l.getTokenCredentials(h.env, "") ensure.Nil(t, err) ensure.DeepEqual(t, credentials.token, "token") + l.tokenReader = strings.NewReader( + `machine api.example.com + login default + password token + `, + ) h.env.Server = "http://api.parse.com" - credentials, err = l.getTokenCredentials(h.env, "") + _, credentials, err = l.getTokenCredentials(h.env, "") ensure.Err(t, err, keyNotFound) + + l = &login{} + h.env.Server = "http://api.example.com/1/" + + l.tokenReader = strings.NewReader( + `machine api.example.com#email + login default + password token + `, + ) + _, credentials, err = l.getTokenCredentials(h.env, "email") + ensure.Nil(t, err) + ensure.DeepEqual(t, credentials.token, "token") + + l.tokenReader = strings.NewReader( + `machine api.example.com#email + login default + password token + `, + ) + h.env.Server = "http://api.parse.com" + _, credentials, err = l.getTokenCredentials(h.env, "email") + ensure.Err(t, err, keyNotFound) + + l = &login{} + h.env.Server = "http://api.example.com/1/" + + l.tokenReader = strings.NewReader( + `machine api.example.com#email + login default + password token1 +machine api.example.com + login default + password token2 +`, + ) + _, credentials, err = l.getTokenCredentials(h.env, "email") + ensure.Nil(t, err) + ensure.DeepEqual(t, credentials.token, "token1") + + l.tokenReader = strings.NewReader( + `machine api.example.com#email + login default + password token1 +machine api.example.com + login default + password token2 +`, + ) + + _, credentials, err = l.getTokenCredentials(h.env, "xmail") + ensure.Nil(t, err) + ensure.DeepEqual(t, credentials.token, "token2") } func TestAuthUserWithToken(t *testing.T) { From 788701267a6fd49ce5cef8a86e0647b65f4aadd0 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 28 Aug 2015 01:10:36 +0000 Subject: [PATCH 028/106] enable downloading code deployed to parse * fetches cloud code for existing app during new app creation * add a download command that can download the current deployed cloud code --- commands.go | 1 + deploy_cmd.go | 3 +- download_cmd.go | 252 +++++++++++++++++++++++++++++++++++++++++++ download_cmd_test.go | 136 +++++++++++++++++++++++ new.go | 12 +++ releases_cmd.go | 48 +++------ releases_cmd_test.go | 57 ++++++---- 7 files changed, 457 insertions(+), 52 deletions(-) create mode 100644 download_cmd.go create mode 100644 download_cmd_test.go diff --git a/commands.go b/commands.go index 3d4acff..9137d0e 100644 --- a/commands.go +++ b/commands.go @@ -29,6 +29,7 @@ http://parse.com`, c.AddCommand(newDefaultCmd(e)) c.AddCommand(newDeployCmd(e)) c.AddCommand(newDevelopCmd(e)) + c.AddCommand(newDownloadCmd(e)) c.AddCommand(newFunctionHooksCmd(e)) c.AddCommand(newGenerateCmd(e)) c.AddCommand(newJsSdkCmd(e)) diff --git a/deploy_cmd.go b/deploy_cmd.go index c46fc8e..38161e3 100644 --- a/deploy_cmd.go +++ b/deploy_cmd.go @@ -309,7 +309,8 @@ type deployInfo struct { ParseVersion string `json:"parseVersion,omitempty"` Checksums deployFileData `json:"checksums,omitempty"` Versions deployFileData `json:"userFiles,omitempty"` - Warning string `json:"warning,omitempty"` + Timestamp string `json:"timestamp,omitempty"` // only populated by get from releases + Warning string `json:"warning,omitempty"` // only populated by post to deploy } func (d *deployCmd) makeNewRelease(info *deployInfo, e *env) (deployInfo, error) { diff --git a/download_cmd.go b/download_cmd.go new file mode 100644 index 0000000..dc46764 --- /dev/null +++ b/download_cmd.go @@ -0,0 +1,252 @@ +package main + +import ( + "crypto/md5" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "path" + "path/filepath" + + "github.com/facebookgo/errgroup" + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type downloadCmd struct { + force bool + release *deployInfo +} + +func (d *downloadCmd) verifyChecksum(path, checksum string) error { + file, err := os.Open(path) + if err != nil { + return stackerr.Wrap(err) + } + defer file.Close() + + h := md5.New() + _, err = io.Copy(h, file) + if err != nil { + return stackerr.Wrap(err) + } + + sum := fmt.Sprintf("%x", h.Sum(nil)) + if sum != checksum { + return stackerr.Newf("Invalid checksum for %s", path) + } + + return nil +} + +func (d *downloadCmd) moveFiles( + e *env, + tempDir string, + release *deployInfo) error { + var wg errgroup.Group + + maxParallel := make(chan struct{}, maxOpenFD) + wg.Add(len(release.Versions.Cloud) + len(release.Versions.Public)) + + moveFile := func(tempDir, kind, file, checksum string) { + defer func() { + wg.Done() + <-maxParallel + }() + + err := os.MkdirAll( + filepath.Join(e.Root, kind, filepath.Dir(file)), + 0755, + ) + if err != nil { + wg.Error(stackerr.Wrap(err)) + return + } + + err = os.Rename( + filepath.Join(tempDir, kind, file), + filepath.Join(e.Root, kind, file), + ) + if err != nil { + wg.Error(stackerr.Wrap(err)) + return + } + + err = d.verifyChecksum( + filepath.Join(e.Root, kind, file), + checksum, + ) + if err != nil { + wg.Error(err) + return + } + } + + for file, checksum := range release.Checksums.Cloud { + maxParallel <- struct{}{} + go moveFile( + tempDir, + cloudDir, + file, + checksum, + ) + } + for file, checksum := range release.Checksums.Public { + maxParallel <- struct{}{} + go moveFile( + tempDir, + hostingDir, + file, + checksum, + ) + } + return wg.Wait() +} + +func (d *downloadCmd) download(e *env, tempDir string, release *deployInfo) error { + var wg errgroup.Group + maxParallel := make(chan struct{}, maxOpenFD) + wg.Add(len(release.Versions.Cloud) + len(release.Versions.Public)) + + downloadHosted := func(file, version, checksum string) { + defer func() { + wg.Done() + <-maxParallel + }() + + v := make(url.Values) + v.Set("version", version) + v.Set("checksum", checksum) + u := &url.URL{ + Path: path.Join("hosted_files", file), + RawQuery: v.Encode(), + } + var content []byte + _, err := e.ParseAPIClient.Get(u, &content) + if err != nil { + wg.Error(err) + return + } + + path := path.Join(tempDir, hostingDir, file) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + wg.Error(stackerr.Wrap(err)) + return + } + if err := ioutil.WriteFile(path, content, 0644); err != nil { + wg.Error(stackerr.Wrap(err)) + return + } + if err := d.verifyChecksum(path, checksum); err != nil { + wg.Error(err) + return + } + } + + downloadScript := func(file, version, checksum string) { + defer func() { + wg.Done() + <-maxParallel + }() + + v := make(url.Values) + v.Set("version", version) + v.Set("checksum", checksum) + u := &url.URL{ + Path: path.Join("scripts", file), + RawQuery: v.Encode(), + } + var content string + _, err := e.ParseAPIClient.Get(u, &content) + if err != nil { + wg.Error(err) + return + } + + path := path.Join(tempDir, cloudDir, file) + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + wg.Error(stackerr.Wrap(err)) + return + } + if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil { + wg.Error(stackerr.Wrap(err)) + return + } + } + + for file, version := range release.Versions.Public { + checksum, ok := release.Checksums.Public[file] + if !ok { + continue + } + maxParallel <- struct{}{} + go downloadHosted(file, version, checksum) + } + + for file, version := range release.Versions.Cloud { + checksum, ok := release.Checksums.Cloud[file] + if !ok { + continue + } + maxParallel <- struct{}{} + go downloadScript(file, version, checksum) + } + + return wg.Wait() +} + +func (d *downloadCmd) run(e *env, c *context) error { + if d.release == nil { + latestRelease, err := (&deployCmd{}).getPrevDeplInfo(e) + if err != nil { + return err + } + d.release = latestRelease + } + + tempDir, err := ioutil.TempDir("", "parse_code_") + if err != nil { + return stackerr.Wrap(err) + } + + err = d.download(e, tempDir, d.release) + if err != nil { + fmt.Fprintln(e.Err, "Failed to download Cloud Code.") + return stackerr.Wrap(err) + } + if !d.force { + fmt.Fprintf(e.Out, "Successfully downloaded Cloud Code to %q.\n", tempDir) + return nil + } + + err = d.moveFiles(e, tempDir, d.release) + if err != nil { + fmt.Fprintf( + e.Out, + `Failed to download Cloud Code to %q. +Sorry! but %s might have corrupted contents. +If you want to download Cloud Code from Parse, +try again without the "-f" option. +`, + e.Root, + e.Root, + ) + return stackerr.Wrap(err) + } + return nil +} + +func newDownloadCmd(e *env) *cobra.Command { + d := &downloadCmd{} + cmd := &cobra.Command{ + Use: "download [app]", + Short: "Downloads the Cloud Code project", + Long: "Downloads the Cloud Code project at a temporary location.", + Run: runWithClient(e, d.run), + } + cmd.Flags().BoolVarP(&d.force, "force", "f", d.force, + "Force will overwrite any content in the current project directory") + return cmd +} diff --git a/download_cmd_test.go b/download_cmd_test.go new file mode 100644 index 0000000..158cd94 --- /dev/null +++ b/download_cmd_test.go @@ -0,0 +1,136 @@ +package main + +import ( + "io/ioutil" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/facebookgo/ensure" + "github.com/facebookgo/parse" +) + +var ( + scriptPath = regexp.MustCompile("/1/scripts/.*") + hostedPath = regexp.MustCompile("/1/hosted_files/.*") +) + +func newDownloadHarness(t testing.TB) (*Harness, *downloadCmd) { + h := createParseProject(t) + ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.FormValue("version"), "version") + switch { + case scriptPath.MatchString(r.URL.Path): + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(`"content"`)), + }, nil + case hostedPath.MatchString(r.URL.Path): + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(`[1, 1, 2, 3, 5, 8, 13]`)), + }, nil + default: + return &http.Response{ + StatusCode: http.StatusNotFound, + Body: ioutil.NopCloser(strings.NewReader(`{"error": "something is wrong"}`)), + }, nil + } + }) + + h.makeEmptyRoot() + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + + d := &downloadCmd{ + release: &deployInfo{ + Versions: deployFileData{ + Cloud: map[string]string{"main.js": "version"}, + Public: map[string]string{"index.html": "version"}, + }, + Checksums: deployFileData{ + Cloud: map[string]string{"main.js": "9a0364b9e99bb480dd25e1f0284c8555"}, + Public: map[string]string{"index.html": "ea46dea1ca5f0b7a728aa3c2a87ae8a1"}, + }, + }, + } + + return h, d +} + +func TestDownload(t *testing.T) { + t.Parallel() + + h, d := newDownloadHarness(t) + defer h.Stop() + + tempDir, err := ioutil.TempDir("", "download_") + ensure.Nil(t, err) + defer func() { + os.RemoveAll(tempDir) + }() + + err = d.download(h.env, tempDir, d.release) + ensure.Nil(t, err) + for file := range d.release.Versions.Cloud { + _, err := os.Open(filepath.Join(tempDir, cloudDir, file)) + ensure.Nil(t, err) + } + for file := range d.release.Versions.Public { + _, err := os.Open(filepath.Join(tempDir, hostingDir, file)) + ensure.Nil(t, err) + } +} + +func TestDownloadMoveFiles(t *testing.T) { + t.Parallel() + h, d := newDownloadHarness(t) + defer h.Stop() + + content := []byte("content") + d.release.Checksums.Cloud["main.js"] = "9a0364b9e99bb480dd25e1f0284c8555" + d.release.Checksums.Public["index.html"] = "9a0364b9e99bb480dd25e1f0284c8555" + + tempDir, err := ioutil.TempDir("", "move_files_") + defer func() { + os.RemoveAll(tempDir) + }() + + err = os.MkdirAll( + filepath.Join(tempDir, hostingDir), + 0755, + ) + ensure.Nil(t, err) + err = os.MkdirAll( + filepath.Join(tempDir, cloudDir), + 0755, + ) + ensure.Nil(t, err) + + err = ioutil.WriteFile( + filepath.Join(tempDir, hostingDir, "index.html"), + content, + 0644, + ) + ensure.Nil(t, err) + err = ioutil.WriteFile( + filepath.Join(tempDir, cloudDir, "main.js"), + content, + 0644, + ) + ensure.Nil(t, err) + + err = d.moveFiles(h.env, tempDir, d.release) + ensure.Nil(t, err) + + readData, err := ioutil.ReadFile( + filepath.Join(h.env.Root, hostingDir, "index.html"), + ) + ensure.DeepEqual(t, readData, content) + readData, err = ioutil.ReadFile( + filepath.Join(h.env.Root, cloudDir, "main.js"), + ) + ensure.DeepEqual(t, readData, content) +} diff --git a/new.go b/new.go index 5357e5c..2939981 100644 --- a/new.go +++ b/new.go @@ -250,6 +250,18 @@ func (n *newCmd) run(e *env) error { } } + if decision == "e" || decision == "existing" { + fmt.Fprintln(e.Out, "Fetching Cloud Code for the selected app from Parse.") + d := &downloadCmd{force: true} + ctx, err := newContext(e, app.Name) + if err != nil { + return stackerr.Wrap(err) + } + if err := d.run(e, ctx); err != nil { + return stackerr.Wrap(err) + } + } + fmt.Fprintf(e.Out, n.cloudCodeHelpMessage(e, app)) return nil } diff --git a/releases_cmd.go b/releases_cmd.go index 2485c5c..17ae857 100644 --- a/releases_cmd.go +++ b/releases_cmd.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "net/url" "sort" @@ -12,24 +11,12 @@ import ( "github.com/spf13/cobra" ) -type userFiles struct { - Cloud map[string]interface{} `json:"cloud"` - Public map[string]interface{} `json:"public"` -} - -type releasesResponse struct { - Version string `json:"version"` - Description string `json:"description"` - Timestamp string `json:"timestamp"` - UserFiles string `json:"userFiles"` -} - type releasesCmd struct { version string } func (r *releasesCmd) printFileNames( - fileVersions map[string]interface{}, + fileVersions map[string]string, e *env) { var files []string for name := range fileVersions { @@ -39,37 +26,32 @@ func (r *releasesCmd) printFileNames( fmt.Fprintln(e.Out, strings.Join(files, "\n")) } -func (r *releasesCmd) printFiles(version string, - releases []releasesResponse, +func (r *releasesCmd) printFiles( + version string, + releases []deployInfo, e *env) error { - var files string + var files deployFileData for _, release := range releases { - if release.Version == version { - files = release.UserFiles + if release.ParseVersion == version { + files = release.Versions break } } - if files == "" { + if len(files.Cloud) == 0 && len(files.Public) == 0 { return stackerr.Newf(`Unable to fetch files for release version: %s Note that you can list files for all releases shown in "parse releases"`, version) } - var versionFileNames userFiles - if err := json.NewDecoder( - strings.NewReader(files), - ).Decode(&versionFileNames); err != nil { - return stackerr.Wrap(err) - } - if len(versionFileNames.Cloud) != 0 { + if len(files.Cloud) != 0 { fmt.Fprintf(e.Out, "Deployed cloud code files:\n") - r.printFileNames(versionFileNames.Cloud, e) + r.printFileNames(files.Cloud, e) } - if len(versionFileNames.Cloud) != 0 && len(versionFileNames.Public) != 0 { + if len(files.Cloud) != 0 && len(files.Public) != 0 { fmt.Fprintln(e.Out) } - if len(versionFileNames.Public) != 0 { + if len(files.Public) != 0 { fmt.Fprintf(e.Out, "Deployed public hosting files:\n") - r.printFileNames(versionFileNames.Public, e) + r.printFileNames(files.Public, e) } return nil } @@ -78,7 +60,7 @@ func (r *releasesCmd) run(e *env, c *context) error { u := &url.URL{ Path: "releases", } - var releasesList []releasesResponse + var releasesList []deployInfo if _, err := e.ParseAPIClient.Get(u, &releasesList); err != nil { return stackerr.Wrap(err) } @@ -95,7 +77,7 @@ func (r *releasesCmd) run(e *env, c *context) error { if release.Description != "" { description = release.Description } - fmt.Fprintf(w, "%s\t%s\t%s\n", release.Version, description, release.Timestamp) + fmt.Fprintf(w, "%s\t%s\t%s\n", release.ParseVersion, description, release.Timestamp) } w.Flush() return nil diff --git a/releases_cmd_test.go b/releases_cmd_test.go index b7f0c72..aebe48e 100644 --- a/releases_cmd_test.go +++ b/releases_cmd_test.go @@ -22,9 +22,9 @@ func newReleasesCmdHarness(t testing.TB) (*Harness, *releasesCmd) { func TestReleasesCmd(t *testing.T) { h, r := newReleasesCmdHarness(t) defer h.Stop() - rows := []releasesResponse{ - {Version: "v1", Description: "version 1", Timestamp: "time 1"}, - {Version: "v2", Description: "version 2", Timestamp: "time 2"}, + rows := []deployInfo{ + {ParseVersion: "v1", Description: "version 1", Timestamp: "time 1"}, + {ParseVersion: "v2", Description: "version 2", Timestamp: "time 2"}, } ht := transportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/releases") @@ -57,23 +57,44 @@ func TestReleasesCmdError(t *testing.T) { func TestReleasesCmdPrintVersion(t *testing.T) { h, r := newReleasesCmdHarness(t) - releases := []releasesResponse{ - {Version: "v1", - UserFiles: `{ - "cloud": {"main.js": "1", "app.js": "1", "views/index.js": "1"} - }`, + releases := []deployInfo{ + { + ParseVersion: "v1", + Versions: deployFileData{ + Cloud: map[string]string{ + "main.js": "1", + "app.js": "1", + "views/index.js": "1", + }, + }, }, - {Version: "v2", - UserFiles: `{ - "cloud": {"main.js": "2", "app.js": "2", "views/docs.js": "1"}, - "public": {"index.html": "2", "docs.html": "2"} - }`, + { + ParseVersion: "v2", + Versions: deployFileData{ + Cloud: map[string]string{ + "main.js": "2", + "app.js": "2", + "views/docs.js": "1", + }, + Public: map[string]string{ + "index.html": "2", + "docs.html": "2", + }, + }, }, - {Version: "v2", - UserFiles: `{ - "cloud": {"v2_main.js": "2", "v2_app.js": "2", "views/v2_docs.js": "2"}, - "public": {"v2_index.html": "2", "v2_docs.html": "2"} - }`, + { + ParseVersion: "v2", + Versions: deployFileData{ + Cloud: map[string]string{ + "v2_main.js": "2", + "v2_app.js": "2", + "views/v2_docs.js": "2", + }, + Public: map[string]string{ + "v2_index.html": "2", + "v2_docs.html": "2", + }, + }, }, } err := r.printFiles("", releases, h.env) From 953b35a2772493eeeae7ee550116b445a6358a77 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sat, 29 Aug 2015 17:56:15 +0000 Subject: [PATCH 029/106] improve messaging in download command * do not force download on new. but print a message * if failed to move even a single file. do not print corrupted message --- download_cmd.go | 58 ++++++++++++++++++++++++++++++++++--------------- new.go | 16 ++++++-------- 2 files changed, 47 insertions(+), 27 deletions(-) diff --git a/download_cmd.go b/download_cmd.go index dc46764..1be9e2a 100644 --- a/download_cmd.go +++ b/download_cmd.go @@ -9,6 +9,7 @@ import ( "os" "path" "path/filepath" + "sync/atomic" "github.com/facebookgo/errgroup" "github.com/facebookgo/stackerr" @@ -48,8 +49,10 @@ func (d *downloadCmd) moveFiles( var wg errgroup.Group maxParallel := make(chan struct{}, maxOpenFD) - wg.Add(len(release.Versions.Cloud) + len(release.Versions.Public)) + numFiles := len(release.Versions.Cloud) + len(release.Versions.Public) + wg.Add(numFiles) + var numErrors int32 moveFile := func(tempDir, kind, file, checksum string) { defer func() { wg.Done() @@ -61,6 +64,7 @@ func (d *downloadCmd) moveFiles( 0755, ) if err != nil { + atomic.AddInt32(&numErrors, 1) wg.Error(stackerr.Wrap(err)) return } @@ -70,6 +74,7 @@ func (d *downloadCmd) moveFiles( filepath.Join(e.Root, kind, file), ) if err != nil { + atomic.AddInt32(&numErrors, 1) wg.Error(stackerr.Wrap(err)) return } @@ -79,6 +84,7 @@ func (d *downloadCmd) moveFiles( checksum, ) if err != nil { + atomic.AddInt32(&numErrors, 1) wg.Error(err) return } @@ -102,7 +108,37 @@ func (d *downloadCmd) moveFiles( checksum, ) } - return wg.Wait() + + if err := wg.Wait(); err != nil { + // could not move a single file so no corruption:w + if int(numErrors) == numFiles { + fmt.Fprintf( + e.Out, + `Failed to download Cloud Code to + %q +Try "parse download" and manually move contents from +the temporary download location. +`, + e.Root, + ) + return nil + } + + fmt.Fprintf( + e.Out, + `Failed to download Cloud Code to + %q + +It might have corrupted contents, due to partially moved files. + +Try "parse download" and manually move contents from +the temporary download location. +`, + e.Root, + ) + return err + } + return nil } func (d *downloadCmd) download(e *env, tempDir string, release *deployInfo) error { @@ -221,21 +257,7 @@ func (d *downloadCmd) run(e *env, c *context) error { return nil } - err = d.moveFiles(e, tempDir, d.release) - if err != nil { - fmt.Fprintf( - e.Out, - `Failed to download Cloud Code to %q. -Sorry! but %s might have corrupted contents. -If you want to download Cloud Code from Parse, -try again without the "-f" option. -`, - e.Root, - e.Root, - ) - return stackerr.Wrap(err) - } - return nil + return stackerr.Wrap(d.moveFiles(e, tempDir, d.release)) } func newDownloadCmd(e *env) *cobra.Command { @@ -247,6 +269,6 @@ func newDownloadCmd(e *env) *cobra.Command { Run: runWithClient(e, d.run), } cmd.Flags().BoolVarP(&d.force, "force", "f", d.force, - "Force will overwrite any content in the current project directory") + "Force will overwrite any files in the current project directory") return cmd } diff --git a/new.go b/new.go index 2939981..4e3a3a6 100644 --- a/new.go +++ b/new.go @@ -251,15 +251,13 @@ func (n *newCmd) run(e *env) error { } if decision == "e" || decision == "existing" { - fmt.Fprintln(e.Out, "Fetching Cloud Code for the selected app from Parse.") - d := &downloadCmd{force: true} - ctx, err := newContext(e, app.Name) - if err != nil { - return stackerr.Wrap(err) - } - if err := d.run(e, ctx); err != nil { - return stackerr.Wrap(err) - } + fmt.Fprintln( + e.Out, + ` +NOTE: If you like to fetch the latest deployed Cloud Code from Parse, +you can use the "parse download" command after finishing the set up. +`, + ) } fmt.Fprintf(e.Out, n.cloudCodeHelpMessage(e, app)) From 6b38fdf61b5235ad8cae54095985982d0b02c8ec Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sat, 29 Aug 2015 20:02:00 +0000 Subject: [PATCH 030/106] print message about default account key * print message about default account key the first time it is configured --- configure_cmd.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/configure_cmd.go b/configure_cmd.go index ddb48c0..37518fe 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -3,6 +3,7 @@ package main import ( "fmt" "io" + "os" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" @@ -34,7 +35,24 @@ func (c *configureCmd) accountKey(e *env) error { if c.tokenReader != nil { l.tokenReader = c.tokenReader } - foundEmail, creds, _ := l.getTokenCredentials(e, email) + foundEmail, creds, err := l.getTokenCredentials(e, email) + if stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) && !c.isDefault { + fmt.Fprintln( + e.Out, + ` + +Looks like you have not configured the default account key yet. +Note that "parse new" and "parse list" can automatically pick up a default key if present. +Otherwise, you'll have to explicitly set the PARSER_EMAIL environment variable +for it to know which account key to use. +Further, if the command line tool cannot find an account key for a configured email it will try to +use the default account key + +To configure the default account key use: + "parse configure accountkey -d"`, + ) + } + if creds != nil { if c.isDefault { fmt.Fprintln( From 051e5302989a0514c8e0f1747363941abb8b2894 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sat, 29 Aug 2015 20:14:40 +0000 Subject: [PATCH 031/106] match substring of output in configure cmd test --- configure_cmd_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/configure_cmd_test.go b/configure_cmd_test.go index cca065f..d70e4cb 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -20,12 +20,11 @@ func TestConfigureAccountKey(t *testing.T) { h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) ensure.Nil(t, c.accountKey(h.env)) - ensure.DeepEqual( + ensure.StringContains( t, h.Out.String(), ` Input your account key or press enter to generate a new one. -Account Key: Successfully stored account key for: "email". `) h.env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) From a3deba6da24c4c002ec19cedd1b517f380255b73 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sat, 29 Aug 2015 20:17:09 +0000 Subject: [PATCH 032/106] bump version to 2.2.5 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 9c6cb0e..90161e0 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - version = "2.2.4" + version = "2.2.5" cloudDir = "cloud" hostingDir = "public" defaultBaseURL = "https://api.parse.com/1/" From c7dbfc908a84ed76c7ec63cd13662201f5d256e5 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 1 Sep 2015 16:42:27 +0000 Subject: [PATCH 033/106] fix releases cmd unmarshaling bug this bug is related to consolidation of deploy & releases cmd https://developers.facebook.com/bugs/1477221272603618/ --- deploy_cmd.go | 3 +-- releases_cmd.go | 48 +++++++++++++++++++++++++------------ releases_cmd_test.go | 57 ++++++++++++++------------------------------ 3 files changed, 52 insertions(+), 56 deletions(-) diff --git a/deploy_cmd.go b/deploy_cmd.go index 38161e3..702c667 100644 --- a/deploy_cmd.go +++ b/deploy_cmd.go @@ -309,8 +309,7 @@ type deployInfo struct { ParseVersion string `json:"parseVersion,omitempty"` Checksums deployFileData `json:"checksums,omitempty"` Versions deployFileData `json:"userFiles,omitempty"` - Timestamp string `json:"timestamp,omitempty"` // only populated by get from releases - Warning string `json:"warning,omitempty"` // only populated by post to deploy + Warning string `json:"warning,omitempty"` // only populated by post to deploy } func (d *deployCmd) makeNewRelease(info *deployInfo, e *env) (deployInfo, error) { diff --git a/releases_cmd.go b/releases_cmd.go index 17ae857..2485c5c 100644 --- a/releases_cmd.go +++ b/releases_cmd.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "net/url" "sort" @@ -11,12 +12,24 @@ import ( "github.com/spf13/cobra" ) +type userFiles struct { + Cloud map[string]interface{} `json:"cloud"` + Public map[string]interface{} `json:"public"` +} + +type releasesResponse struct { + Version string `json:"version"` + Description string `json:"description"` + Timestamp string `json:"timestamp"` + UserFiles string `json:"userFiles"` +} + type releasesCmd struct { version string } func (r *releasesCmd) printFileNames( - fileVersions map[string]string, + fileVersions map[string]interface{}, e *env) { var files []string for name := range fileVersions { @@ -26,32 +39,37 @@ func (r *releasesCmd) printFileNames( fmt.Fprintln(e.Out, strings.Join(files, "\n")) } -func (r *releasesCmd) printFiles( - version string, - releases []deployInfo, +func (r *releasesCmd) printFiles(version string, + releases []releasesResponse, e *env) error { - var files deployFileData + var files string for _, release := range releases { - if release.ParseVersion == version { - files = release.Versions + if release.Version == version { + files = release.UserFiles break } } - if len(files.Cloud) == 0 && len(files.Public) == 0 { + if files == "" { return stackerr.Newf(`Unable to fetch files for release version: %s Note that you can list files for all releases shown in "parse releases"`, version) } - if len(files.Cloud) != 0 { + var versionFileNames userFiles + if err := json.NewDecoder( + strings.NewReader(files), + ).Decode(&versionFileNames); err != nil { + return stackerr.Wrap(err) + } + if len(versionFileNames.Cloud) != 0 { fmt.Fprintf(e.Out, "Deployed cloud code files:\n") - r.printFileNames(files.Cloud, e) + r.printFileNames(versionFileNames.Cloud, e) } - if len(files.Cloud) != 0 && len(files.Public) != 0 { + if len(versionFileNames.Cloud) != 0 && len(versionFileNames.Public) != 0 { fmt.Fprintln(e.Out) } - if len(files.Public) != 0 { + if len(versionFileNames.Public) != 0 { fmt.Fprintf(e.Out, "Deployed public hosting files:\n") - r.printFileNames(files.Public, e) + r.printFileNames(versionFileNames.Public, e) } return nil } @@ -60,7 +78,7 @@ func (r *releasesCmd) run(e *env, c *context) error { u := &url.URL{ Path: "releases", } - var releasesList []deployInfo + var releasesList []releasesResponse if _, err := e.ParseAPIClient.Get(u, &releasesList); err != nil { return stackerr.Wrap(err) } @@ -77,7 +95,7 @@ func (r *releasesCmd) run(e *env, c *context) error { if release.Description != "" { description = release.Description } - fmt.Fprintf(w, "%s\t%s\t%s\n", release.ParseVersion, description, release.Timestamp) + fmt.Fprintf(w, "%s\t%s\t%s\n", release.Version, description, release.Timestamp) } w.Flush() return nil diff --git a/releases_cmd_test.go b/releases_cmd_test.go index aebe48e..b7f0c72 100644 --- a/releases_cmd_test.go +++ b/releases_cmd_test.go @@ -22,9 +22,9 @@ func newReleasesCmdHarness(t testing.TB) (*Harness, *releasesCmd) { func TestReleasesCmd(t *testing.T) { h, r := newReleasesCmdHarness(t) defer h.Stop() - rows := []deployInfo{ - {ParseVersion: "v1", Description: "version 1", Timestamp: "time 1"}, - {ParseVersion: "v2", Description: "version 2", Timestamp: "time 2"}, + rows := []releasesResponse{ + {Version: "v1", Description: "version 1", Timestamp: "time 1"}, + {Version: "v2", Description: "version 2", Timestamp: "time 2"}, } ht := transportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/releases") @@ -57,44 +57,23 @@ func TestReleasesCmdError(t *testing.T) { func TestReleasesCmdPrintVersion(t *testing.T) { h, r := newReleasesCmdHarness(t) - releases := []deployInfo{ - { - ParseVersion: "v1", - Versions: deployFileData{ - Cloud: map[string]string{ - "main.js": "1", - "app.js": "1", - "views/index.js": "1", - }, - }, + releases := []releasesResponse{ + {Version: "v1", + UserFiles: `{ + "cloud": {"main.js": "1", "app.js": "1", "views/index.js": "1"} + }`, }, - { - ParseVersion: "v2", - Versions: deployFileData{ - Cloud: map[string]string{ - "main.js": "2", - "app.js": "2", - "views/docs.js": "1", - }, - Public: map[string]string{ - "index.html": "2", - "docs.html": "2", - }, - }, + {Version: "v2", + UserFiles: `{ + "cloud": {"main.js": "2", "app.js": "2", "views/docs.js": "1"}, + "public": {"index.html": "2", "docs.html": "2"} + }`, }, - { - ParseVersion: "v2", - Versions: deployFileData{ - Cloud: map[string]string{ - "v2_main.js": "2", - "v2_app.js": "2", - "views/v2_docs.js": "2", - }, - Public: map[string]string{ - "v2_index.html": "2", - "v2_docs.html": "2", - }, - }, + {Version: "v2", + UserFiles: `{ + "cloud": {"v2_main.js": "2", "v2_app.js": "2", "views/v2_docs.js": "2"}, + "public": {"v2_index.html": "2", "v2_docs.html": "2"} + }`, }, } err := r.printFiles("", releases, h.env) From 6bd5cdc01f34d8cac054227d07a8ace4f6755efc Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 4 Sep 2015 01:35:22 +0000 Subject: [PATCH 034/106] update to new go-update library --- ios_symbol_uploader.go | 2 +- update_cmd.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ios_symbol_uploader.go b/ios_symbol_uploader.go index 786fbd8..2ccc31d 100644 --- a/ios_symbol_uploader.go +++ b/ios_symbol_uploader.go @@ -182,7 +182,7 @@ func (i *iosSymbolUploader) symbolConversionTool(baseDir string, resp, err = http.Get(osxSymbolConverterDownloadURL) if err == nil { defer resp.Body.Close() - err = update.Apply(resp.Body, &update.Options{TargetPath: toolPath}) + err = update.Apply(resp.Body, update.Options{TargetPath: toolPath}) } } } else { diff --git a/update_cmd.go b/update_cmd.go index 4bcd2f9..1957399 100644 --- a/update_cmd.go +++ b/update_cmd.go @@ -78,7 +78,7 @@ func (u *updateCmd) updateCLI(e *env) (bool, error) { return false, stackerr.Newf("Update failed with error: %v", err) } defer resp.Body.Close() - err = update.Apply(resp.Body, &update.Options{TargetPath: exec}) + err = update.Apply(resp.Body, update.Options{TargetPath: exec}) if err != nil { return false, stackerr.Newf("Update failed with error: %v", err) } From f237b8d21d6579cddd5795b859035c53af9db099 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 4 Sep 2015 03:07:54 +0000 Subject: [PATCH 035/106] bump version to 2.2.6 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 90161e0..735c8e1 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - version = "2.2.5" + version = "2.2.6" cloudDir = "cloud" hostingDir = "public" defaultBaseURL = "https://api.parse.com/1/" From bd9c5d4f4e2d50875b65f7cc1e3d86a012d154a9 Mon Sep 17 00:00:00 2001 From: Nikita Lutsenko Date: Thu, 3 Sep 2015 20:52:03 -0700 Subject: [PATCH 036/106] Added code coverage via Codecov. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 21470e3..68e0e54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,9 @@ language: go go: - 1.4.2 script: - - go test -v ./... + - go test -v ./... -coverprofile=coverage.txt -covermode=atomic +after_success: + - bash <(curl -s https://codecov.io/bash) before_deploy: - go get github.com/mitchellh/gox - gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" -build-toolchain From 88d6b21dc734df60f0545986f98d4c2746166d5e Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 17 Sep 2015 16:40:07 -0700 Subject: [PATCH 037/106] log which parse command was used --- main.go | 13 +++++++------ utils.go | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 735c8e1..df7a38b 100644 --- a/main.go +++ b/main.go @@ -100,7 +100,13 @@ func main() { } e.ParseAPIClient = apiClient - message, err := checkIfSupported(&e, version) + var rootCmd *cobra.Command + switch e.Type { + case legacyParseFormat, parseFormat: + rootCmd = parseRootCmd(&e) + } + + message, err := checkIfSupported(&e, version, os.Args[1:]...) if err != nil { fmt.Fprintln(e.Err, err) os.Exit(1) @@ -109,11 +115,6 @@ func main() { fmt.Fprintln(e.Err, message) } - var rootCmd *cobra.Command - switch e.Type { - case legacyParseFormat, parseFormat: - rootCmd = parseRootCmd(&e) - } if err := rootCmd.Execute(); err != nil { // Error is already printed in Execute() os.Exit(1) diff --git a/utils.go b/utils.go index 5b32f24..753b6bc 100644 --- a/utils.go +++ b/utils.go @@ -112,9 +112,10 @@ func getHostFromURL(urlStr, email string) (string, error) { return server, nil } -func checkIfSupported(e *env, version string) (string, error) { +func checkIfSupported(e *env, version string, args ...string) (string, error) { v := make(url.Values) v.Set("version", version) + v.Set("other", strings.Join(args, " ")) req := &http.Request{ Method: "GET", URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, From a97c9833730f26ee5fe9a09b46274817ef720d17 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 17 Sep 2015 17:55:11 -0700 Subject: [PATCH 038/106] make improvements to 'new' command * parametrize 'new' command * let default action for existing apps be download latest deployed cloude code * allow download to specific location * allow 'new' to have a mode to create skelton with config only --- add.go | 13 +++++- add_cmd_test.go | 4 +- apps.go | 14 ++++-- apps_test.go | 2 +- config_test.go | 2 +- default_cmd_test.go | 4 +- download_cmd.go | 52 +++++++++++++--------- jssdk_cmd_test.go | 2 +- list_cmd_test.go | 2 +- new.go | 102 ++++++++++++++++++++++++++++++++++---------- new_cmd.go | 18 +++----- new_cmd_test.go | 10 ++--- 12 files changed, 152 insertions(+), 73 deletions(-) diff --git a/add.go b/add.go index 786332a..19c35fa 100644 --- a/add.go +++ b/add.go @@ -25,11 +25,20 @@ func (a *addCmd) addSelectedApp( return stackerr.Newf("Unknown project type: %d.", e.Type) } -func (a *addCmd) selectApp(e *env) (*app, error) { +func (a *addCmd) selectApp(e *env, appName string) (*app, error) { apps, err := a.apps.restFetchApps(e) if err != nil { return nil, err } + if appName != "" { + for _, app := range apps { + if app.Name == appName { + return app, nil + } + } + return nil, stackerr.Newf("You are not a collaborator on app: %s", appName) + } + app, err := a.apps.selectApp(apps, "Select an App to add to config: ", e) if err != nil { return nil, err @@ -42,7 +51,7 @@ func (a *addCmd) run(e *env, args []string) error { if err := a.apps.login.authUser(e); err != nil { return err } - app, err := a.selectApp(e) + app, err := a.selectApp(e, "") // TODO (can also make this configurable) if err != nil { return err } diff --git a/add_cmd_test.go b/add_cmd_test.go index 06fc794..1fa9006 100644 --- a/add_cmd_test.go +++ b/add_cmd_test.go @@ -58,7 +58,7 @@ func newAddCmdHarnessWithConfig(t testing.TB) (*Harness, *addCmd, error) { h, _ := newAddCmdHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "B"}, false)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "B"}, false, true)) return h, &a, nil } @@ -115,7 +115,7 @@ func newLegacyAddCmdHarnessWithConfig(t testing.TB) (*Harness, *addCmd, error) { h, _ := newAddCmdHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "B"}, false)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "B"}, false, true)) ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) ensure.Nil(t, ioutil.WriteFile( diff --git a/apps.go b/apps.go index e97849b..2a71f7b 100644 --- a/apps.go +++ b/apps.go @@ -222,7 +222,7 @@ func (a *apps) restCreateApp(e *env, appName string) (*app, error) { return &res, nil } -func (a *apps) createApp(e *env) (*app, error) { +func (a *apps) createApp(e *env, givenName string) (*app, error) { apps, err := a.restFetchApps(e) if err != nil { return nil, err @@ -232,10 +232,16 @@ func (a *apps) createApp(e *env) (*app, error) { appNames[app.Name] = struct{}{} } for i := 0; i < numRetries; i++ { - appName, err := a.getAppName(e) - if err != nil { - return nil, err + var appName string + if givenName != "" { + appName = givenName + } else { + appName, err = a.getAppName(e) + if err != nil { + return nil, err + } } + if _, ok := appNames[appName]; ok { fmt.Fprintf(e.Out, `Hey! you already created an app named %q. diff --git a/apps_test.go b/apps_test.go index 0aaf8d0..948dfc7 100644 --- a/apps_test.go +++ b/apps_test.go @@ -234,7 +234,7 @@ func TestCreateNewApp(t *testing.T) { a := defaultApps h.env.In = ioutil.NopCloser(strings.NewReader("D")) - app, err := a.createApp(h.env) + app, err := a.createApp(h.env, "") ensure.Nil(t, err) ensure.Nil(t, a.printApp(h.env, app)) diff --git a/config_test.go b/config_test.go index 4ba54f8..85be2d7 100644 --- a/config_test.go +++ b/config_test.go @@ -27,7 +27,7 @@ func TestConfigFromDirFormatType(t *testing.T) { h.makeEmptyRoot() defer h.Stop() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) c, err := configFromDir(h.env.Root) ensure.Nil(t, err) diff --git a/default_cmd_test.go b/default_cmd_test.go index 449f87c..b39ff76 100644 --- a/default_cmd_test.go +++ b/default_cmd_test.go @@ -27,7 +27,7 @@ func newDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConfig) { h := newHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) config := newDefaultParseConfig(t, h) h.Out.Reset() @@ -97,7 +97,7 @@ func newLegacyDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConf h := newHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.env.Root, legacyConfigFile), []byte("{}"), diff --git a/download_cmd.go b/download_cmd.go index 1be9e2a..e1e2972 100644 --- a/download_cmd.go +++ b/download_cmd.go @@ -17,8 +17,9 @@ import ( ) type downloadCmd struct { - force bool - release *deployInfo + release *deployInfo + destination string + force bool } func (d *downloadCmd) verifyChecksum(path, checksum string) error { @@ -44,7 +45,7 @@ func (d *downloadCmd) verifyChecksum(path, checksum string) error { func (d *downloadCmd) moveFiles( e *env, - tempDir string, + destination string, release *deployInfo) error { var wg errgroup.Group @@ -53,7 +54,7 @@ func (d *downloadCmd) moveFiles( wg.Add(numFiles) var numErrors int32 - moveFile := func(tempDir, kind, file, checksum string) { + moveFile := func(destination, kind, file, checksum string) { defer func() { wg.Done() <-maxParallel @@ -70,7 +71,7 @@ func (d *downloadCmd) moveFiles( } err = os.Rename( - filepath.Join(tempDir, kind, file), + filepath.Join(destination, kind, file), filepath.Join(e.Root, kind, file), ) if err != nil { @@ -93,7 +94,7 @@ func (d *downloadCmd) moveFiles( for file, checksum := range release.Checksums.Cloud { maxParallel <- struct{}{} go moveFile( - tempDir, + destination, cloudDir, file, checksum, @@ -102,7 +103,7 @@ func (d *downloadCmd) moveFiles( for file, checksum := range release.Checksums.Public { maxParallel <- struct{}{} go moveFile( - tempDir, + destination, hostingDir, file, checksum, @@ -141,7 +142,7 @@ the temporary download location. return nil } -func (d *downloadCmd) download(e *env, tempDir string, release *deployInfo) error { +func (d *downloadCmd) download(e *env, destination string, release *deployInfo) error { var wg errgroup.Group maxParallel := make(chan struct{}, maxOpenFD) wg.Add(len(release.Versions.Cloud) + len(release.Versions.Public)) @@ -166,7 +167,7 @@ func (d *downloadCmd) download(e *env, tempDir string, release *deployInfo) erro return } - path := path.Join(tempDir, hostingDir, file) + path := path.Join(destination, hostingDir, file) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { wg.Error(stackerr.Wrap(err)) return @@ -201,7 +202,7 @@ func (d *downloadCmd) download(e *env, tempDir string, release *deployInfo) erro return } - path := path.Join(tempDir, cloudDir, file) + path := path.Join(destination, cloudDir, file) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { wg.Error(stackerr.Wrap(err)) return @@ -234,30 +235,35 @@ func (d *downloadCmd) download(e *env, tempDir string, release *deployInfo) erro } func (d *downloadCmd) run(e *env, c *context) error { - if d.release == nil { - latestRelease, err := (&deployCmd{}).getPrevDeplInfo(e) + var err error + + latestRelease := d.release + if latestRelease == nil { + latestRelease, err = (&deployCmd{}).getPrevDeplInfo(e) if err != nil { return err } - d.release = latestRelease } - tempDir, err := ioutil.TempDir("", "parse_code_") - if err != nil { - return stackerr.Wrap(err) + destination := d.destination + if destination == "" { + destination, err = ioutil.TempDir("", "parse_code_") + if err != nil { + return stackerr.Wrap(err) + } } - err = d.download(e, tempDir, d.release) + err = d.download(e, destination, latestRelease) if err != nil { fmt.Fprintln(e.Err, "Failed to download Cloud Code.") return stackerr.Wrap(err) } if !d.force { - fmt.Fprintf(e.Out, "Successfully downloaded Cloud Code to %q.\n", tempDir) + fmt.Fprintf(e.Out, "Successfully downloaded Cloud Code to %q.\n", destination) return nil } - return stackerr.Wrap(d.moveFiles(e, tempDir, d.release)) + return stackerr.Wrap(d.moveFiles(e, destination, latestRelease)) } func newDownloadCmd(e *env) *cobra.Command { @@ -265,10 +271,14 @@ func newDownloadCmd(e *env) *cobra.Command { cmd := &cobra.Command{ Use: "download [app]", Short: "Downloads the Cloud Code project", - Long: "Downloads the Cloud Code project at a temporary location.", - Run: runWithClient(e, d.run), + Long: `Downloads the Cloud Code project at a given location, +or at a temporary location if nothing is explicitly provided through the -l flag. +`, + Run: runWithClient(e, d.run), } cmd.Flags().BoolVarP(&d.force, "force", "f", d.force, "Force will overwrite any files in the current project directory") + cmd.Flags().StringVarP(&d.destination, "location", "l", d.destination, + "Download Cloud Code project at the given location.") return cmd } diff --git a/jssdk_cmd_test.go b/jssdk_cmd_test.go index 7657b19..226ffc3 100644 --- a/jssdk_cmd_test.go +++ b/jssdk_cmd_test.go @@ -44,7 +44,7 @@ func newJsSdkHarnessWithConfig(t testing.TB) (*Harness, *context) { h := newJsSdkHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) h.Out.Reset() c, err := configFromDir(h.env.Root) diff --git a/list_cmd_test.go b/list_cmd_test.go index b43bfe4..ef70b3d 100644 --- a/list_cmd_test.go +++ b/list_cmd_test.go @@ -67,6 +67,6 @@ func TestPrintListNoApps(t *testing.T) { h.makeEmptyRoot() defer h.Stop() n := &newCmd{} - n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false) + n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true) ensure.Nil(t, l.printListOfApps(h.env)) } diff --git a/new.go b/new.go index 4e3a3a6..a0830e7 100644 --- a/new.go +++ b/new.go @@ -13,6 +13,11 @@ import ( type newCmd struct { addApplication bool + + noCode bool // do not setup/download code + createNewApp bool // create a new app + parseAppName string // name of parse app + codeLocation string // location of cloud code project } var newProjectFiles = []struct { @@ -52,16 +57,26 @@ you can test that it works, with: func (n *newCmd) getCloudCodeDir(e *env, appName string, isNew bool) (string, error) { var cloudCodeDir string - fmt.Fprintf(e.Out, - `Awesome! Now it's time to setup some Cloud Code for the app: %q, + if isNew { + fmt.Fprintf(e.Out, + `Awesome! Now it's time to setup some Cloud Code for the app: %q, Next we will create a directory to hold your Cloud Code. Please enter the name to use for this directory, or hit ENTER to use %q as the directory name. Directory Name: `, - appName, - appName, - ) + appName, + appName, + ) + } else { + fmt.Fprintf(e.Out, + `Please enter the name of the folder where we can download the latest deployed +Cloud Code for your app %q + +Directory Name: `, + appName, + ) + } fmt.Scanf("%s\n", &cloudCodeDir) cloudCodeDir = strings.TrimSpace(cloudCodeDir) @@ -134,7 +149,14 @@ func (n *newCmd) createConfigWithContent(path, content string) error { return nil } -func (n *newCmd) promptCreateNewApp(e *env) (string, error) { +func (n *newCmd) promptCreateNewApp(e *env, nonInteractive bool) (string, error) { + if nonInteractive { + if n.createNewApp { + return "new", nil + } + return "existing", nil + } + msg := `"new" and "existing" are the only valid options. Please try again ...` @@ -155,7 +177,7 @@ Type "(n)ew" or "(e)xisting": `, return "", stackerr.New(msg) } -func (n *newCmd) setupSample(e *env, app *app, isNew bool) error { +func (n *newCmd) setupSample(e *env, app *app, isNew bool, nonInteractive bool) error { found := isProjectDir(getProjectRoot(e, e.Root)) if !found { root := getLegacyProjectRoot(e, e.Root) @@ -170,9 +192,46 @@ Please refrain from creating a Parse project inside another Parse project. ) } + var ( + cloudCodeDir string + err error + ) + + if nonInteractive { + cloudCodeDir = n.codeLocation + } else { + cloudCodeDir, err = n.getCloudCodeDir(e, app.Name, isNew) + if err != nil { + return err + } + } + e.Root = filepath.Join(e.Root, cloudCodeDir) + switch e.Type { case parseFormat: - return n.cloneSampleCloudCode(e, app, isNew) + if !isNew && !n.noCode { + // if parse app was already created try to fetch cloud code and populate dir + e.ParseAPIClient = e.ParseAPIClient.WithCredentials( + parse.MasterKey{ + ApplicationID: app.ApplicationID, + MasterKey: app.MasterKey, + }, + ) + + d := &downloadCmd{destination: e.Root} + err = d.run(e, nil) + if err != nil { + fmt.Fprintln( + e.Out, + ` +NOTE: If you like to fetch the latest deployed Cloud Code from Parse, +you can use the "parse download" command after finishing the set up. +This will download Cloud Code to a temporary location. +`, + ) + } + } + return n.cloneSampleCloudCode(e, app, isNew, isNew && !n.noCode) } return stackerr.Newf("Unknown project type: %d", e.Type) } @@ -212,7 +271,8 @@ func (n *newCmd) run(e *env) error { err error ) - decision, err := n.promptCreateNewApp(e) + nonInteractive := n.parseAppName != "" && n.codeLocation != "" + decision, err := n.promptCreateNewApp(e, nonInteractive) if err != nil { return err } @@ -221,12 +281,12 @@ func (n *newCmd) run(e *env) error { switch decision { case "new", "n": isNew = true - app, err = apps.createApp(e) + app, err = apps.createApp(e, n.parseAppName) if err != nil { return err } case "existing", "e": - app, err = addCmd.selectApp(e) + app, err = addCmd.selectApp(e, n.parseAppName) if err != nil { return err } @@ -234,7 +294,7 @@ func (n *newCmd) run(e *env) error { e.Type = parseFormat - if err := n.setupSample(e, app, isNew); err != nil { + if err := n.setupSample(e, app, isNew, nonInteractive); err != nil { return err } if err := n.configureSample(addCmd, app, nil, e); err != nil { @@ -250,16 +310,6 @@ func (n *newCmd) run(e *env) error { } } - if decision == "e" || decision == "existing" { - fmt.Fprintln( - e.Out, - ` -NOTE: If you like to fetch the latest deployed Cloud Code from Parse, -you can use the "parse download" command after finishing the set up. -`, - ) - } - fmt.Fprintf(e.Out, n.cloudCodeHelpMessage(e, app)) return nil } @@ -272,5 +322,13 @@ func newNewCmd(e *env) *cobra.Command { Long: `Creates a new Parse app and adds Cloud Code to an existing Parse app.`, Run: runNoArgs(e, nc.run), } + cmd.Flags().BoolVarP(&nc.noCode, "config", "c", nc.noCode, + "Create a Cloud Code project only with configuration.") + cmd.Flags().BoolVarP(&nc.createNewApp, "new", "n", nc.createNewApp, + "Set this flag to true if you want to create a new Parse app.") + cmd.Flags().StringVarP(&nc.parseAppName, "app", "a", nc.parseAppName, + "Name of the Parse app you want to create or set up Cloud Code project for.") + cmd.Flags().StringVarP(&nc.codeLocation, "loc", "l", nc.codeLocation, + "Location at which the Cloud Code project will be setup.") return cmd } diff --git a/new_cmd.go b/new_cmd.go index 106a7ba..5d9c57e 100644 --- a/new_cmd.go +++ b/new_cmd.go @@ -8,17 +8,8 @@ import ( "github.com/facebookgo/stackerr" ) -func (n *newCmd) cloneSampleCloudCode(e *env, app *app, isNew bool) error { - cloudCodeDir, err := n.getCloudCodeDir(e, app.Name, isNew) - if err != nil { - return err - } - e.Root = filepath.Join(e.Root, cloudCodeDir) - - if err != nil { - return err - } - err = os.MkdirAll(e.Root, 0755) +func (n *newCmd) cloneSampleCloudCode(e *env, app *app, isNew bool, dumpTemplate bool) error { + err := os.MkdirAll(e.Root, 0755) if err != nil { return stackerr.Wrap(err) } @@ -44,6 +35,11 @@ func (n *newCmd) cloneSampleCloudCode(e *env, app *app, isNew bool) error { return err } + // no need to setup the template code + if !dumpTemplate { + return nil + } + for _, info := range newProjectFiles { sampleDir := filepath.Join(e.Root, info.dirname) if _, err := os.Stat(sampleDir); err != nil { diff --git a/new_cmd_test.go b/new_cmd_test.go index eec3da1..b511acf 100644 --- a/new_cmd_test.go +++ b/new_cmd_test.go @@ -24,7 +24,7 @@ func TestNewCmdDirs(t *testing.T) { h, n := newNewCmdHarness(t) defer h.Stop() - ensure.Nil(t, n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + ensure.Nil(t, n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) var err error @@ -40,7 +40,7 @@ func TestNewCmdContent(t *testing.T) { h, n := newNewCmdHarness(t) defer h.Stop() - ensure.Nil(t, n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false)) + ensure.Nil(t, n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) for _, newProjectFile := range newProjectFiles { content, err := ioutil.ReadFile( @@ -92,16 +92,16 @@ func TestShouldCreateNewApp(t *testing.T) { n := &newCmd{} h.env.In = ioutil.NopCloser(strings.NewReader("new")) - decision, err := n.promptCreateNewApp(h.env) + decision, err := n.promptCreateNewApp(h.env, false) ensure.Nil(t, err) ensure.DeepEqual(t, decision, "new") h.env.In = ioutil.NopCloser(strings.NewReader("existing")) - decision, err = n.promptCreateNewApp(h.env) + decision, err = n.promptCreateNewApp(h.env, false) ensure.Nil(t, err) ensure.DeepEqual(t, decision, "existing") h.env.In = ioutil.NopCloser(strings.NewReader("other")) - _, err = n.promptCreateNewApp(h.env) + _, err = n.promptCreateNewApp(h.env, false) ensure.Err(t, err, regexp.MustCompile("are the only valid options")) } From 5d8778dff7195907d8e16df16b901d1ba2209558 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 17 Sep 2015 19:16:56 -0700 Subject: [PATCH 039/106] [list] list should take argument * list should take app name as argument and print its properties * additionally if no argument is provided print all available apps --- apps.go | 27 ++++++++++++++++----------- apps_test.go | 3 ++- list_cmd.go | 10 +++++++--- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/apps.go b/apps.go index 2a71f7b..379ee0b 100644 --- a/apps.go +++ b/apps.go @@ -57,8 +57,9 @@ func selectionString(appNames []string) string { } func (a *apps) printApp(e *env, params *app) error { - w := new(tabwriter.Writer) + fmt.Fprintf(e.Out, "Properties of the app %q:\n", params.Name) + w := new(tabwriter.Writer) w.Init(e.Out, 0, 8, 0, '\t', 0) fmt.Fprintf(w, "Name\t%s\n", params.Name) fmt.Fprintf(w, "DashboardURL\t%s\n", params.DashboardURL) @@ -173,21 +174,25 @@ func (a *apps) selectApp(apps []*app, msg string, e *env) (*app, error) { return nil, stackerr.Newf("Please try again. Please select from among the listed apps.") } -func (a *apps) showApps(e *env) error { +func (a *apps) showApps(e *env, appName string) error { apps, err := a.restFetchApps(e) if err != nil { return err } - fmt.Fprintln(e.Out, "The following apps are currently owned by you:") - app, err := a.selectApp(apps, - "Select an app to view its properties: ", - e, - ) - if err != nil { - return err + if appName != "" { + for _, app := range apps { + if app.Name == appName { + return a.printApp(e, app) + } + } } - fmt.Fprintf(e.Out, "Properties of app: %q\n", app.Name) - return a.printApp(e, app) + + fmt.Fprintf( + e.Out, + "These are the apps you currently have access to:\n%s", + selectionString(allApps(apps)), + ) + return nil } func (a *apps) getAppName(e *env) (string, error) { diff --git a/apps_test.go b/apps_test.go index 948dfc7..4c6904d 100644 --- a/apps_test.go +++ b/apps_test.go @@ -242,7 +242,8 @@ func TestCreateNewApp(t *testing.T) { `Please choose a name for your Parse app. Note that this name will appear on the Parse website, but it does not have to be the same as your mobile app's public name. -Name: Name D +Name: Properties of the app "D": +Name D DashboardURL https://api.example.com/dashboard/D ApplicationID applicationID.D ClientKey clientKey.D diff --git a/list_cmd.go b/list_cmd.go index a297ab5..62869a9 100644 --- a/list_cmd.go +++ b/list_cmd.go @@ -28,14 +28,18 @@ func (l *listCmd) printListOfApps(e *env) error { return nil } -func (l *listCmd) run(e *env) error { +func (l *listCmd) run(e *env, args []string) error { l.printListOfApps(e) var apps apps if err := apps.login.authUser(e); err != nil { return err } - return apps.showApps(e) + var appName string + if len(args) > 0 { + appName = args[0] + } + return apps.showApps(e, appName) } func newListCmd(e *env) *cobra.Command { @@ -46,6 +50,6 @@ func newListCmd(e *env) *cobra.Command { Long: `Lists Parse apps and aliases added to the current Cloud Code directory when executed inside the directory. Additionally, it prints the list of Parse apps associated with current Parse account.`, - Run: runNoArgs(e, l.run), + Run: runWithArgs(e, l.run), } } From a7223359dd7c74efa0816181cd817aa4c44176ca Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 17 Sep 2015 19:32:34 -0700 Subject: [PATCH 040/106] [new] print curl command only for default template * print curl command only when default template is output as code --- download_cmd.go | 6 ++++++ new.go | 32 +++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/download_cmd.go b/download_cmd.go index e1e2972..4bf6d4d 100644 --- a/download_cmd.go +++ b/download_cmd.go @@ -2,6 +2,7 @@ package main import ( "crypto/md5" + "errors" "fmt" "io" "io/ioutil" @@ -22,6 +23,8 @@ type downloadCmd struct { force bool } +var errNoFiles = errors.New("Nothing to download. Not yet deployed to the app.") + func (d *downloadCmd) verifyChecksum(path, checksum string) error { file, err := os.Open(path) if err != nil { @@ -243,6 +246,9 @@ func (d *downloadCmd) run(e *env, c *context) error { if err != nil { return err } + if len(latestRelease.Versions.Cloud) == 0 && len(latestRelease.Versions.Public) == 0 { + return errNoFiles + } } destination := d.destination diff --git a/new.go b/new.go index a0830e7..4b6b5b4 100644 --- a/new.go +++ b/new.go @@ -177,7 +177,7 @@ Type "(n)ew" or "(e)xisting": `, return "", stackerr.New(msg) } -func (n *newCmd) setupSample(e *env, app *app, isNew bool, nonInteractive bool) error { +func (n *newCmd) setupSample(e *env, app *app, isNew bool, nonInteractive bool) (bool, error) { found := isProjectDir(getProjectRoot(e, e.Root)) if !found { root := getLegacyProjectRoot(e, e.Root) @@ -185,7 +185,7 @@ func (n *newCmd) setupSample(e *env, app *app, isNew bool, nonInteractive bool) found = err == nil } if found { - return stackerr.New( + return false, stackerr.New( `Detected that you are already inside a Parse project. Please refrain from creating a Parse project inside another Parse project. `, @@ -202,13 +202,14 @@ Please refrain from creating a Parse project inside another Parse project. } else { cloudCodeDir, err = n.getCloudCodeDir(e, app.Name, isNew) if err != nil { - return err + return false, err } } e.Root = filepath.Join(e.Root, cloudCodeDir) switch e.Type { case parseFormat: + dumpTemplate := false if !isNew && !n.noCode { // if parse app was already created try to fetch cloud code and populate dir e.ParseAPIClient = e.ParseAPIClient.WithCredentials( @@ -221,19 +222,24 @@ Please refrain from creating a Parse project inside another Parse project. d := &downloadCmd{destination: e.Root} err = d.run(e, nil) if err != nil { - fmt.Fprintln( - e.Out, - ` + if err == errNoFiles { + dumpTemplate = true + } else { + fmt.Fprintln( + e.Out, + ` NOTE: If you like to fetch the latest deployed Cloud Code from Parse, you can use the "parse download" command after finishing the set up. This will download Cloud Code to a temporary location. `, - ) + ) + } } } - return n.cloneSampleCloudCode(e, app, isNew, isNew && !n.noCode) + dumpTemplate = (isNew || dumpTemplate) && !n.noCode + return dumpTemplate, n.cloneSampleCloudCode(e, app, isNew, dumpTemplate) } - return stackerr.Newf("Unknown project type: %d", e.Type) + return false, stackerr.Newf("Unknown project type: %d", e.Type) } func (n *newCmd) configureSample( @@ -294,7 +300,8 @@ func (n *newCmd) run(e *env) error { e.Type = parseFormat - if err := n.setupSample(e, app, isNew, nonInteractive); err != nil { + dumpTemplate, err := n.setupSample(e, app, isNew, nonInteractive) + if err != nil { return err } if err := n.configureSample(addCmd, app, nil, e); err != nil { @@ -310,7 +317,10 @@ func (n *newCmd) run(e *env) error { } } - fmt.Fprintf(e.Out, n.cloudCodeHelpMessage(e, app)) + if dumpTemplate { + fmt.Fprintf(e.Out, n.cloudCodeHelpMessage(e, app)) + } + return nil } From 919c4f5dd8d0f985553251c09267f3031229c7f7 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 17 Sep 2015 20:00:19 -0700 Subject: [PATCH 041/106] [configure] set first ever account key = default --- configure_cmd.go | 48 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index 37518fe..a621d31 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -36,21 +36,9 @@ func (c *configureCmd) accountKey(e *env) error { l.tokenReader = c.tokenReader } foundEmail, creds, err := l.getTokenCredentials(e, email) + firstEverConfigure := false if stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) && !c.isDefault { - fmt.Fprintln( - e.Out, - ` - -Looks like you have not configured the default account key yet. -Note that "parse new" and "parse list" can automatically pick up a default key if present. -Otherwise, you'll have to explicitly set the PARSER_EMAIL environment variable -for it to know which account key to use. -Further, if the command line tool cannot find an account key for a configured email it will try to -use the default account key - -To configure the default account key use: - "parse configure accountkey -d"`, - ) + firstEverConfigure = true } if creds != nil { @@ -80,7 +68,37 @@ for email: %q fmt.Fprintf(e.Out, "Successfully stored account key for: %q.\n", email) } } - return stackerr.Wrap(err) + if err != nil { + fmt.Fprintln(e.Err, "Could not save account key.") + return stackerr.Wrap(err) + } + + if firstEverConfigure { + fmt.Fprintln( + e.Out, + ` +Looks like this is the first time you have configured an account key. +Note that "parse new" and "parse list" can automatically pick up a default key if present. +Otherwise, you'll have to explicitly set the PARSER_EMAIL environment variable +for them to pick the correct account key. +Further, if the command line tool cannot find an account key for a configured email it will try to +use the default account key. +Hence, we are automatically configuring the default account key to be the same as current account key. +`, + ) + err = c.login.storeCredentials(e, "", &credentials{token: token}) + if err != nil { + fmt.Fprintln(e.Err, "Could not save account key.") + return stackerr.Wrap(err) + } + fmt.Fprintln(e.Out, `Successfully configured the default account key. +To change the default account key in future use: + + "parse configure accountkey -d" +`) + } + + return nil } func (c *configureCmd) parserEmail(e *env, args []string) error { From 2b9cc4c0cc81142a440f6f23ee2f1889dfd9e52d Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 18 Sep 2015 09:06:18 -0700 Subject: [PATCH 042/106] [configure] allow configuring project type * allow user to configure the project type for given code --- configure_cmd.go | 83 ++++++++++++++++++++++++++++++++++++++++--- configure_cmd_test.go | 32 +++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index a621d31..44d6394 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -4,6 +4,8 @@ import ( "fmt" "io" "os" + "sort" + "strconv" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" @@ -119,6 +121,70 @@ func (c *configureCmd) parserEmail(e *env, args []string) error { return nil } +func (c *configureCmd) projectType(e *env, args []string) error { + config, err := configFromDir(e.Root) + if err != nil { + return err + } + if len(args) > 1 { + return stackerr.Newf("Invalid args: %v, only an optional project type argument is expected.", args) + } + validTypes := map[string]int{"parse": parseFormat} + invertedTypes := map[int]string{parseFormat: "parse"} + numKeys := len(validTypes) + var validKeys []string + for key := range validTypes { + validKeys = append(validKeys, key) + } + sort.Strings(validKeys) + var selectionString string + for n, key := range validKeys { + selectionString += fmt.Sprintf("%d: %s\n", 1+n, key) + } + + selectedProjectType := -1 + if len(args) != 0 { + projectType, ok := validTypes[args[0]] + if !ok { + return stackerr.Newf("Invalid projectType: %v, valid types are: \n%s", selectionString) + } + selectedProjectType = projectType + } + + for i := 0; i < 3; i++ { + fmt.Fprintf(e.Out, `Select from the listed project types: +%s +Enter a number between 1 and %d: `, + selectionString, + numKeys, + ) + var selection string + fmt.Fscanf(e.In, "%s\n", &selection) + num, err := strconv.Atoi(selection) + if err != nil || num < 1 || num > numKeys { + fmt.Fprintf(e.Err, "Invalid selection. Please enter a number between 1 and %d\n", numKeys) + continue + } + projectType, ok := validTypes[validKeys[num-1]] + if !ok { + return stackerr.Newf("Invalid projectType: %v, valid types are: \n%s", selectionString) + } + selectedProjectType = projectType + break + } + if selectedProjectType == -1 { + return stackerr.Newf("Could not make a selection. Please try again.") + } + + config.getProjectConfig().Type = selectedProjectType + if err := storeProjectConfig(e, config); err != nil { + fmt.Fprintln(e.Err, "Could not save selected project type to project config") + return err + } + fmt.Fprintf(e.Out, "Successfully set project type to: %v\n", invertedTypes[selectedProjectType]) + return nil +} + func newConfigureCmd(e *env) *cobra.Command { var c configureCmd @@ -143,12 +209,21 @@ func newConfigureCmd(e *env) *cobra.Command { cmd.AddCommand(keyCmd) emailCmd := &cobra.Command{ - Use: "email", - Short: "Configures the parser email for this project", - Long: "Configures the parser email for current project.", - Run: runWithArgs(e, c.parserEmail), + Use: "email", + Short: "Configures the parser email for this project", + Long: "Configures the parser email for current project.", + Run: runWithArgs(e, c.parserEmail), + Aliases: []string{"user"}, } cmd.AddCommand(emailCmd) + projectCmd := &cobra.Command{ + Use: "project", + Short: "Set the project type to one among listed options", + Long: "Set the project type to one among listed options. For instance, 'parse'", + Run: runWithArgs(e, c.projectType), + } + cmd.AddCommand(projectCmd) + return cmd } diff --git a/configure_cmd_test.go b/configure_cmd_test.go index d70e4cb..fffb992 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -118,3 +118,35 @@ func TestParserEmail(t *testing.T) { ensure.Err(t, c.parserEmail(h.env, nil), regexp.MustCompile("Invalid args:")) ensure.Err(t, c.parserEmail(h.env, []string{"a", "b"}), regexp.MustCompile("Invalid args:")) } + +func TestProjectType(t *testing.T) { + t.Parallel() + h := newHarness(t) + defer h.Stop() + + h.makeEmptyRoot() + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, false)) + + c := &configureCmd{} + err := c.projectType(h.env, []string{"1", "2"}) + ensure.Err(t, err, regexp.MustCompile("only an optional project type argument is expected")) + + h.env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) + err = c.projectType(h.env, nil) + ensure.StringContains(t, h.Err.String(), "Invalid selection. Please enter a number") + ensure.Err(t, err, regexp.MustCompile("Could not make a selection. Please try again.")) + h.Err.Reset() + h.Out.Reset() + + h.env.In = ioutil.NopCloser(strings.NewReader("0\n")) + err = c.projectType(h.env, nil) + ensure.StringContains(t, h.Err.String(), "Please enter a number between 1 and") + ensure.Err(t, err, regexp.MustCompile("Could not make a selection. Please try again.")) + h.Err.Reset() + h.Out.Reset() + + h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) + err = c.projectType(h.env, nil) + ensure.StringContains(t, h.Out.String(), "Successfully set project type to: parse") + ensure.Nil(t, err) +} From 44d835b0f28c2f6dcbc11b4b711136eab7f0e277 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 18 Sep 2015 09:19:39 -0700 Subject: [PATCH 043/106] [help] update help various help messages --- configure_cmd.go | 4 ++-- list_cmd.go | 7 ++++--- new.go | 8 +++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index 44d6394..97a14c2 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -209,7 +209,7 @@ func newConfigureCmd(e *env) *cobra.Command { cmd.AddCommand(keyCmd) emailCmd := &cobra.Command{ - Use: "email", + Use: "email [id]", Short: "Configures the parser email for this project", Long: "Configures the parser email for current project.", Run: runWithArgs(e, c.parserEmail), @@ -218,7 +218,7 @@ func newConfigureCmd(e *env) *cobra.Command { cmd.AddCommand(emailCmd) projectCmd := &cobra.Command{ - Use: "project", + Use: "project [type]", Short: "Set the project type to one among listed options", Long: "Set the project type to one among listed options. For instance, 'parse'", Run: runWithArgs(e, c.projectType), diff --git a/list_cmd.go b/list_cmd.go index 62869a9..156bd2c 100644 --- a/list_cmd.go +++ b/list_cmd.go @@ -45,11 +45,12 @@ func (l *listCmd) run(e *env, args []string) error { func newListCmd(e *env) *cobra.Command { l := listCmd{} return &cobra.Command{ - Use: "list", - Short: "Lists Parse apps associated with current Parse account", + Use: "list [app]", + Short: "Lists properties of given Parse app and Parse apps associated with given project", Long: `Lists Parse apps and aliases added to the current Cloud Code directory when executed inside the directory. -Additionally, it prints the list of Parse apps associated with current Parse account.`, +Additionally, it prints the list of Parse apps associated with current Parse account. +If an optional app name is provided, it prints all the keys for that app.`, Run: runWithArgs(e, l.run), } } diff --git a/new.go b/new.go index 4b6b5b4..bda796b 100644 --- a/new.go +++ b/new.go @@ -328,9 +328,11 @@ func newNewCmd(e *env) *cobra.Command { nc := newCmd{addApplication: true} cmd := &cobra.Command{ Use: "new", - Short: "Creates a new Parse app and adds Cloud Code to an existing Parse app", - Long: `Creates a new Parse app and adds Cloud Code to an existing Parse app.`, - Run: runNoArgs(e, nc.run), + Short: "Adds Cloud Code to an existing Parse app, additional can create a new Parse app", + Long: `Adds Cloud Code to an existing Parse app, additional can create a new Parse app. +You can also use it in non-interactive mode by using the various flags available. +`, + Run: runNoArgs(e, nc.run), } cmd.Flags().BoolVarP(&nc.noCode, "config", "c", nc.noCode, "Create a Cloud Code project only with configuration.") From 8f24c9cf230fd5df3d190b96df6f6f8ba6c06b9c Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 18 Sep 2015 09:37:33 -0700 Subject: [PATCH 044/106] [new-clone] remove extraneous parameters in clone sample --- add_cmd_test.go | 4 ++-- config_test.go | 2 +- configure_cmd_test.go | 2 +- default_cmd_test.go | 4 ++-- jssdk_cmd_test.go | 2 +- list_cmd_test.go | 2 +- new.go | 2 +- new_cmd.go | 2 +- new_cmd_test.go | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/add_cmd_test.go b/add_cmd_test.go index 1fa9006..70b9293 100644 --- a/add_cmd_test.go +++ b/add_cmd_test.go @@ -58,7 +58,7 @@ func newAddCmdHarnessWithConfig(t testing.TB) (*Harness, *addCmd, error) { h, _ := newAddCmdHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "B"}, false, true)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) return h, &a, nil } @@ -115,7 +115,7 @@ func newLegacyAddCmdHarnessWithConfig(t testing.TB) (*Harness, *addCmd, error) { h, _ := newAddCmdHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "B"}, false, true)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) ensure.Nil(t, ioutil.WriteFile( diff --git a/config_test.go b/config_test.go index 85be2d7..f1f515f 100644 --- a/config_test.go +++ b/config_test.go @@ -27,7 +27,7 @@ func TestConfigFromDirFormatType(t *testing.T) { h.makeEmptyRoot() defer h.Stop() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) c, err := configFromDir(h.env.Root) ensure.Nil(t, err) diff --git a/configure_cmd_test.go b/configure_cmd_test.go index fffb992..43e4547 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -125,7 +125,7 @@ func TestProjectType(t *testing.T) { defer h.Stop() h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, false)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, false)) c := &configureCmd{} err := c.projectType(h.env, []string{"1", "2"}) diff --git a/default_cmd_test.go b/default_cmd_test.go index b39ff76..4c25232 100644 --- a/default_cmd_test.go +++ b/default_cmd_test.go @@ -27,7 +27,7 @@ func newDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConfig) { h := newHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) config := newDefaultParseConfig(t, h) h.Out.Reset() @@ -97,7 +97,7 @@ func newLegacyDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConf h := newHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.env.Root, legacyConfigFile), []byte("{}"), diff --git a/jssdk_cmd_test.go b/jssdk_cmd_test.go index 226ffc3..4ca9e12 100644 --- a/jssdk_cmd_test.go +++ b/jssdk_cmd_test.go @@ -44,7 +44,7 @@ func newJsSdkHarnessWithConfig(t testing.TB) (*Harness, *context) { h := newJsSdkHarness(t) h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) + ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) h.Out.Reset() c, err := configFromDir(h.env.Root) diff --git a/list_cmd_test.go b/list_cmd_test.go index ef70b3d..b2ed337 100644 --- a/list_cmd_test.go +++ b/list_cmd_test.go @@ -67,6 +67,6 @@ func TestPrintListNoApps(t *testing.T) { h.makeEmptyRoot() defer h.Stop() n := &newCmd{} - n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true) + n.cloneSampleCloudCode(h.env, true) ensure.Nil(t, l.printListOfApps(h.env)) } diff --git a/new.go b/new.go index bda796b..28c3e7f 100644 --- a/new.go +++ b/new.go @@ -237,7 +237,7 @@ This will download Cloud Code to a temporary location. } } dumpTemplate = (isNew || dumpTemplate) && !n.noCode - return dumpTemplate, n.cloneSampleCloudCode(e, app, isNew, dumpTemplate) + return dumpTemplate, n.cloneSampleCloudCode(e, dumpTemplate) } return false, stackerr.Newf("Unknown project type: %d", e.Type) } diff --git a/new_cmd.go b/new_cmd.go index 5d9c57e..54fc680 100644 --- a/new_cmd.go +++ b/new_cmd.go @@ -8,7 +8,7 @@ import ( "github.com/facebookgo/stackerr" ) -func (n *newCmd) cloneSampleCloudCode(e *env, app *app, isNew bool, dumpTemplate bool) error { +func (n *newCmd) cloneSampleCloudCode(e *env, dumpTemplate bool) error { err := os.MkdirAll(e.Root, 0755) if err != nil { return stackerr.Wrap(err) diff --git a/new_cmd_test.go b/new_cmd_test.go index b511acf..663b811 100644 --- a/new_cmd_test.go +++ b/new_cmd_test.go @@ -24,7 +24,7 @@ func TestNewCmdDirs(t *testing.T) { h, n := newNewCmdHarness(t) defer h.Stop() - ensure.Nil(t, n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) + ensure.Nil(t, n.cloneSampleCloudCode(h.env, true)) var err error @@ -40,7 +40,7 @@ func TestNewCmdContent(t *testing.T) { h, n := newNewCmdHarness(t) defer h.Stop() - ensure.Nil(t, n.cloneSampleCloudCode(h.env, &app{Name: "test"}, false, true)) + ensure.Nil(t, n.cloneSampleCloudCode(h.env, true)) for _, newProjectFile := range newProjectFiles { content, err := ioutil.ReadFile( From 1d98bcaf4169cc8b0b77784e9496d8d61c31ca1e Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 21 Sep 2015 07:04:32 -0700 Subject: [PATCH 045/106] [new] rename some config variable to better ones --- new.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/new.go b/new.go index 28c3e7f..8f6a168 100644 --- a/new.go +++ b/new.go @@ -334,9 +334,9 @@ You can also use it in non-interactive mode by using the various flags available `, Run: runNoArgs(e, nc.run), } - cmd.Flags().BoolVarP(&nc.noCode, "config", "c", nc.noCode, - "Create a Cloud Code project only with configuration.") - cmd.Flags().BoolVarP(&nc.createNewApp, "new", "n", nc.createNewApp, + cmd.Flags().BoolVarP(&nc.noCode, "init", "i", nc.noCode, + "Create a Cloud Code project only with configuration, no code.") + cmd.Flags().BoolVarP(&nc.createNewApp, "create", "c", nc.createNewApp, "Set this flag to true if you want to create a new Parse app.") cmd.Flags().StringVarP(&nc.parseAppName, "app", "a", nc.parseAppName, "Name of the Parse app you want to create or set up Cloud Code project for.") From 79549323e1a739e10b1f21d843834e006bccfe76 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 21 Sep 2015 07:05:16 -0700 Subject: [PATCH 046/106] [release] bump version to 2.2.7 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index df7a38b..9d55fe5 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - version = "2.2.6" + version = "2.2.7" cloudDir = "cloud" hostingDir = "public" defaultBaseURL = "https://api.parse.com/1/" From 39062a567470e248d96e4657c66b6019cf8d77e6 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 21 Sep 2015 07:11:39 -0700 Subject: [PATCH 047/106] [readme] update commands and documentation --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c9b85aa..4843b96 100644 --- a/README.md +++ b/README.md @@ -31,16 +31,18 @@ The following commands are currently available in the `Parse Command Line Tool`: default Sets or gets the default Parse app deploy Deploys a Parse app develop Monitors for changes to code and deploys, also tails parse logs + download Downloads the Cloud Code project functions List Cloud Code functions and function webhooks generate Generates a sample express app in the current project directory jssdk Sets the Parse JavaScript SDK version to use in Cloud Code - list Lists Parse apps associated with current Parse account + list Lists properties of the given Parse app and Parse apps associated with given project logs Prints out recent log messages - new Creates a new Parse app and adds Cloud Code to an existing Parse app + migrate Migrate project config format to the preferred format + new Adds Cloud Code to an existing Parse app, can also create a new Parse app releases Gets the releases for a Parse app rollback Rolls back the version for the given app symbols Uploads symbol files - triggers Lists Cloud Code triggers and trigger webhooks + triggers List Cloud Code triggers and trigger webhooks update Updates this tool to the latest version version Gets the Command Line Tools version help Help about any command From 1be502cdd07443ec81e60c87eb09dc88f7ecd02d Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 21 Sep 2015 08:50:49 -0700 Subject: [PATCH 048/106] refactor add & new commands --- add.go | 13 +++++++++---- new.go | 16 +++++++++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/add.go b/add.go index 19c35fa..7a4e352 100644 --- a/add.go +++ b/add.go @@ -12,14 +12,18 @@ type addCmd struct { } func (a *addCmd) addSelectedApp( - app *app, + name string, + appConfig appConfig, args []string, e *env, ) error { switch e.Type { case legacyParseFormat, parseFormat: - appConfig := a.getParseAppConfig(app) - return a.addSelectedParseApp(app.Name, appConfig, args, e) + parseAppConfig, ok := appConfig.(*parseAppConfig) + if !ok { + return stackerr.New("invalid parse app config passed.") + } + return a.addSelectedParseApp(name, parseAppConfig, args, e) } return stackerr.Newf("Unknown project type: %d.", e.Type) @@ -55,7 +59,8 @@ func (a *addCmd) run(e *env, args []string) error { if err != nil { return err } - return a.addSelectedApp(app, args, e) + appConfig := a.getParseAppConfig(app) + return a.addSelectedApp(app.Name, appConfig, args, e) } func newAddCmd(e *env) *cobra.Command { diff --git a/new.go b/new.go index 8f6a168..1ee243e 100644 --- a/new.go +++ b/new.go @@ -244,18 +244,23 @@ This will download Cloud Code to a temporary location. func (n *newCmd) configureSample( add *addCmd, - app *app, + name string, + appConfig appConfig, args []string, e *env, ) error { - if err := add.addSelectedApp(app, args, e); err != nil { + if err := add.addSelectedApp(name, appConfig, args, e); err != nil { return err } + masterKey, err := appConfig.getMasterKey(e) + if err != nil { + return err + } e.ParseAPIClient = e.ParseAPIClient.WithCredentials( parse.MasterKey{ - ApplicationID: app.ApplicationID, - MasterKey: app.MasterKey, + ApplicationID: appConfig.getApplicationID(), + MasterKey: masterKey, }, ) @@ -299,12 +304,13 @@ func (n *newCmd) run(e *env) error { } e.Type = parseFormat + appConfig := addCmd.getParseAppConfig(app) dumpTemplate, err := n.setupSample(e, app, isNew, nonInteractive) if err != nil { return err } - if err := n.configureSample(addCmd, app, nil, e); err != nil { + if err := n.configureSample(addCmd, app.Name, appConfig, nil, e); err != nil { return err } if token := apps.login.credentials.token; token != "" { From 9fdb80796e47544066a3d8dd5c46821fd631ea97 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 21 Sep 2015 09:10:14 -0700 Subject: [PATCH 049/106] [new] refactor new cmd setup sample refactor new cmd setup sample to pass appConfig instead of app --- new.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/new.go b/new.go index 1ee243e..a70b0d2 100644 --- a/new.go +++ b/new.go @@ -177,7 +177,13 @@ Type "(n)ew" or "(e)xisting": `, return "", stackerr.New(msg) } -func (n *newCmd) setupSample(e *env, app *app, isNew bool, nonInteractive bool) (bool, error) { +func (n *newCmd) setupSample( + e *env, + name string, + appConfig appConfig, + isNew bool, + nonInteractive bool, +) (bool, error) { found := isProjectDir(getProjectRoot(e, e.Root)) if !found { root := getLegacyProjectRoot(e, e.Root) @@ -200,7 +206,7 @@ Please refrain from creating a Parse project inside another Parse project. if nonInteractive { cloudCodeDir = n.codeLocation } else { - cloudCodeDir, err = n.getCloudCodeDir(e, app.Name, isNew) + cloudCodeDir, err = n.getCloudCodeDir(e, name, isNew) if err != nil { return false, err } @@ -212,10 +218,14 @@ Please refrain from creating a Parse project inside another Parse project. dumpTemplate := false if !isNew && !n.noCode { // if parse app was already created try to fetch cloud code and populate dir + masterKey, err := appConfig.getMasterKey(e) + if err != nil { + return false, err + } e.ParseAPIClient = e.ParseAPIClient.WithCredentials( parse.MasterKey{ - ApplicationID: app.ApplicationID, - MasterKey: app.MasterKey, + ApplicationID: appConfig.getApplicationID(), + MasterKey: masterKey, }, ) @@ -306,7 +316,7 @@ func (n *newCmd) run(e *env) error { e.Type = parseFormat appConfig := addCmd.getParseAppConfig(app) - dumpTemplate, err := n.setupSample(e, app, isNew, nonInteractive) + dumpTemplate, err := n.setupSample(e, app.Name, appConfig, isNew, nonInteractive) if err != nil { return err } From 7d50b9eff45e31967f6a456d373d8230147df75b Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 21 Sep 2015 09:47:59 -0700 Subject: [PATCH 050/106] [add] allow add cmd to be non-interactive --- add.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/add.go b/add.go index 7a4e352..f82a2e5 100644 --- a/add.go +++ b/add.go @@ -55,7 +55,15 @@ func (a *addCmd) run(e *env, args []string) error { if err := a.apps.login.authUser(e); err != nil { return err } - app, err := a.selectApp(e, "") // TODO (can also make this configurable) + var appName string + if len(args) > 1 { + return stackerr.New("Only an optional Parse app name is expected.") + } + if len(args) == 1 { + appName = args[0] + } + + app, err := a.selectApp(e, appName) if err != nil { return err } From 4677ecd112e7ee0be5f35ffd7613d417c461d859 Mon Sep 17 00:00:00 2001 From: Joe Lutz Date: Tue, 22 Sep 2015 11:46:43 -0500 Subject: [PATCH 051/106] grammar police to the rescue --- new.go | 6 +++--- new_cmd.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/new.go b/new.go index a70b0d2..62a921b 100644 --- a/new.go +++ b/new.go @@ -14,7 +14,7 @@ import ( type newCmd struct { addApplication bool - noCode bool // do not setup/download code + noCode bool // do not set up/download code createNewApp bool // create a new app parseAppName string // name of parse app codeLocation string // location of cloud code project @@ -59,7 +59,7 @@ func (n *newCmd) getCloudCodeDir(e *env, appName string, isNew bool) (string, er var cloudCodeDir string if isNew { fmt.Fprintf(e.Out, - `Awesome! Now it's time to setup some Cloud Code for the app: %q, + `Awesome! Now it's time to set up some Cloud Code for the app: %q, Next we will create a directory to hold your Cloud Code. Please enter the name to use for this directory, or hit ENTER to use %q as the directory name. @@ -357,6 +357,6 @@ You can also use it in non-interactive mode by using the various flags available cmd.Flags().StringVarP(&nc.parseAppName, "app", "a", nc.parseAppName, "Name of the Parse app you want to create or set up Cloud Code project for.") cmd.Flags().StringVarP(&nc.codeLocation, "loc", "l", nc.codeLocation, - "Location at which the Cloud Code project will be setup.") + "Location at which the Cloud Code project will be set up.") return cmd } diff --git a/new_cmd.go b/new_cmd.go index 54fc680..0f1cbe5 100644 --- a/new_cmd.go +++ b/new_cmd.go @@ -35,7 +35,7 @@ func (n *newCmd) cloneSampleCloudCode(e *env, dumpTemplate bool) error { return err } - // no need to setup the template code + // no need to set up the template code if !dumpTemplate { return nil } From 96ab5d8f87e4240e45e4b55fee0bab44a397ae1a Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 21 Sep 2015 22:12:57 +0000 Subject: [PATCH 052/106] [configure] subcommand to create webhooks * adds a subcommand "hooks" to configure command to create webhooks from a csv config file * an optional file path can be provided as an argument ```bash parse configure hooks [path] ``` * if path is not provided it can read from stdin - so one can pipe input using piping operator like ```bash echo "delete,yolo"|parse configure hooks echo -e "delete,yolo\npost,yolo,https://yolo"|pare configure hooks ``` - also you can just type the command and it waits to read from stdin ```bash parse configure hooks Since a webhooks config file was not provided reading from stdin. ``` * adds a bunch of tests * in strict mode if deploying hook fails, the entire operation aborts at that point * normally, it tries to catch errors for instance, it provides protection against: - deleting a non-existant webhook [refrains from performing delete] - trying to create an already existing webhook [updates the current webhook with new url] - trying to modify a non-existant webhooks [creates it with given url] NOTE: It is important to note that cloud code deploy & webhook deploy change a global config object, and these operations are not thread safe. so changes to webhooks should happen sequentially. using the cli ensure this very easily, so you do not write your own custom scripts, and then hit the concurrency issues --- configure_cmd.go | 362 ++++++++++++++++++++++++++++++++++++++++++ configure_cmd_test.go | 361 +++++++++++++++++++++++++++++++++++++++++ functions_cmd.go | 49 +++--- functions_cmd_test.go | 15 +- triggers_cmd.go | 48 +++--- triggers_cmd_test.go | 16 +- 6 files changed, 800 insertions(+), 51 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index 97a14c2..b5b616e 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -1,20 +1,49 @@ package main import ( + "bufio" + "errors" "fmt" "io" + "io/ioutil" + "net/url" "os" + "path" "sort" "strconv" + "strings" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) +var ( + errInvalidFormat = errors.New( + `invalid format. valid formats should look like: +[put|post],functionName,https_url +delete,functionName +[put|post],className,triggerName,https_url +delete,className,triggerName +`) + + errPostToPut = errors.New( + `a hook with given name already exists: cannot create a new one. +`) + + errPutToPost = errors.New( + `a hook with the given name does not exist yet: cannot update the url. +`) + + errNotExist = errors.New( + `a hook with the given name does not exist. cannot delete it. + `) +) + type configureCmd struct { login login isDefault bool tokenReader io.Reader // for testing + hooksStrict bool } func (c *configureCmd) accountKey(e *env) error { @@ -185,6 +214,328 @@ Enter a number between 1 and %d: `, return nil } +type hookOperation struct { + method string + function *functionHook + trigger *triggerHook +} + +func (c *configureCmd) checkTriggerName(s string) error { + switch strings.ToLower(s) { + case "beforesave", "beforedelete", "aftersave", "afterdelete": + return nil + } + return stackerr.Newf( + `invalid trigger name: %v. + This is the list of valid trigger names: + beforeSave + afterSave + beforeDelete + afterDelete +`, + s, + ) +} + +func (c *configureCmd) postOrPutHook( + e *env, + hooksOps []*hookOperation, + fields ...string, +) (bool, []*hookOperation, error) { + restOp := strings.ToUpper(fields[0]) + if restOp != "POST" && restOp != "PUT" { + return false, nil, stackerr.Wrap(errInvalidFormat) + } + + switch len(fields) { + case 3: + hooksOps = append(hooksOps, &hookOperation{ + method: restOp, + function: &functionHook{FunctionName: fields[1], URL: fields[2]}, + }) + return true, hooksOps, nil + + case 4: + if err := c.checkTriggerName(fields[2]); err != nil { + return false, nil, err + } + hooksOps = append(hooksOps, &hookOperation{ + method: restOp, + trigger: &triggerHook{ClassName: fields[1], TriggerName: fields[2], URL: fields[3]}, + }) + return true, hooksOps, nil + } + return false, nil, stackerr.Wrap(errInvalidFormat) + +} + +func (c *configureCmd) deleteHook( + e *env, + hooksOps []*hookOperation, + fields ...string, +) (bool, []*hookOperation, error) { + restOp := strings.ToUpper(fields[0]) + if restOp != "DELETE" { + return false, nil, stackerr.Wrap(errInvalidFormat) + } + + switch len(fields) { + case 2: + hooksOps = append(hooksOps, &hookOperation{ + method: "DELETE", + function: &functionHook{FunctionName: fields[1]}, + }) + return true, hooksOps, nil + case 3: + if err := c.checkTriggerName(fields[2]); err != nil { + return false, nil, err + } + hooksOps = append(hooksOps, &hookOperation{ + method: "DELETE", + trigger: &triggerHook{ClassName: fields[1], TriggerName: fields[2]}, + }) + return true, hooksOps, nil + } + + return false, nil, stackerr.Wrap(errInvalidFormat) +} + +func (c *configureCmd) appendHookOperation( + e *env, + op string, + hooks []*hookOperation, +) (bool, []*hookOperation, error) { + op = strings.TrimSpace(op) + if op == "" { + return false, hooks, nil + } + + fields := strings.SplitN(op, ",", 4) + restOp := strings.ToLower(fields[0]) + + switch restOp { + case "post", "put": + if len(fields) > 2 && strings.HasPrefix(fields[2], "https://") { + fields[2] = strings.Join(fields[2:], ",") + fields = fields[:3] + } + return c.postOrPutHook(e, hooks, fields...) + + case "delete": + return c.deleteHook(e, hooks, fields...) + } + return false, nil, stackerr.Wrap(errInvalidFormat) +} + +func (c *configureCmd) createHooksOperations( + e *env, + reader io.Reader, +) ([]*hookOperation, error) { + scanner := bufio.NewScanner(reader) + var ( + hooksOps []*hookOperation + added bool + err error + ) + lineNum := 0 + for scanner.Scan() { + lineNum++ + line := scanner.Text() + added, hooksOps, err = c.appendHookOperation(e, line, hooksOps) + if err != nil { + return nil, err + } + if !added { + fmt.Fprintf(e.Out, "Ignoring line: %d\n", lineNum) + } + } + if err := scanner.Err(); err != nil { + return nil, stackerr.Wrap(err) + } + return hooksOps, nil +} + +func (c *configureCmd) checkStrictMode(restOp string, exists bool) (string, bool, error) { + restOp = strings.ToUpper(restOp) + if !exists { + if restOp == "PUT" { + if c.hooksStrict { + return "", false, stackerr.Wrap(errPutToPost) + } + return "POST", true, nil + } + if restOp == "DELETE" { + if c.hooksStrict { + return "", false, stackerr.Wrap(errNotExist) + } + return "DELETE", true, nil + } + } else if restOp == "POST" { + if c.hooksStrict { + return "", false, stackerr.Wrap(errPostToPut) + } + return "PUT", true, nil + } + return restOp, false, nil +} + +func (c *configureCmd) functionHookExists(e *env, name string) (bool, error) { + functionsURL, err := url.Parse(path.Join(defaultFunctionsURL, name)) + if err != nil { + return false, stackerr.Wrap(err) + } + var results struct { + Results []*functionHook `json:"results,omitempty"` + } + _, err = e.ParseAPIClient.Get(functionsURL, &results) + if err != nil { + if strings.Contains(err.Error(), "is defined") { + return false, nil + } + return false, stackerr.Wrap(err) + } + for _, result := range results.Results { + if result.URL != "" && result.FunctionName == name { + return true, nil + } + } + return false, nil +} + +func (c *configureCmd) deployFunctionHook(e *env, op *hookOperation) error { + if op.function == nil { + return stackerr.New("cannot deploy nil function hook") + } + exists, err := c.functionHookExists(e, op.function.FunctionName) + if err != nil { + return err + } + + restOp, suppressed, err := c.checkStrictMode(op.method, exists) + if err != nil { + return err + } + + function := &functionHooksCmd{Function: op.function} + switch restOp { + case "POST": + return function.functionHooksCreate(e, nil) + case "PUT": + return function.functionHooksUpdate(e, nil) + case "DELETE": + if suppressed { + return nil + } + return function.functionHooksDelete(e, nil) + } + return stackerr.Wrap(errInvalidFormat) +} + +func (c *configureCmd) triggerHookExists(e *env, className, triggerName string) (bool, error) { + triggersURL, err := url.Parse(path.Join(defaultTriggersURL, className, triggerName)) + if err != nil { + return false, stackerr.Wrap(err) + } + var results struct { + Results []*triggerHook `json:"results,omitempty"` + } + _, err = e.ParseAPIClient.Get(triggersURL, &results) + if err != nil { + if strings.Contains(err.Error(), "is defined") { + return false, nil + } + return false, stackerr.Wrap(err) + } + for _, result := range results.Results { + if result.URL != "" && result.ClassName == className && result.TriggerName == triggerName { + return true, nil + } + } + return false, nil +} + +func (c *configureCmd) deployTriggerHook(e *env, op *hookOperation) error { + if op.trigger == nil { + return stackerr.New("cannot deploy nil trigger hook") + } + + exists, err := c.triggerHookExists(e, op.trigger.ClassName, op.trigger.TriggerName) + if err != nil { + return err + } + restOp, suppressed, err := c.checkStrictMode(op.method, exists) + if err != nil { + return err + } + + trigger := &triggerHooksCmd{Trigger: op.trigger, All: false} + switch restOp { + case "POST": + return trigger.triggerHooksCreate(e, nil) + case "PUT": + return trigger.triggerHooksUpdate(e, nil) + case "DELETE": + if suppressed { + return nil + } + return trigger.triggerHooksDelete(e, nil) + } + return stackerr.Wrap(errInvalidFormat) + +} + +func (c *configureCmd) deployWebhooksConfig(e *env, hooksOps []*hookOperation) error { + for _, op := range hooksOps { + if op.function == nil && op.trigger == nil { + return stackerr.New("hook operation is neither a function, not a trigger.") + } + if op.function != nil && op.trigger != nil { + return stackerr.New("a hook cannot be both a function and a trigger.") + } + if op.function != nil { + if err := c.deployFunctionHook(e, op); err != nil { + return err + } + } else { + if err := c.deployTriggerHook(e, op); err != nil { + return err + } + } + fmt.Fprintln(e.Out) + } + return nil +} + +func (c *configureCmd) hooksCmd(e *env, ctx *context, args []string) error { + if len(args) > 1 { + return fmt.Errorf("Invalid args: %v, only an optional hooks config file is expected.", args) + } + reader := e.In + if len(args) == 1 { + file, err := os.Open(args[0]) + if err != nil { + return stackerr.Wrap(err) + } + reader = ioutil.NopCloser(file) + } else { + fmt.Fprintln(e.Out, "Since a webhooks config file was not provided reading from stdin.") + } + hooksOps, err := c.createHooksOperations(e, reader) + if err != nil { + return err + } + err = c.deployWebhooksConfig(e, hooksOps) + if err != nil { + fmt.Fprintln( + e.Out, + "Failed to deploy the webhooks config. Please try again...", + ) + return err + } + fmt.Fprintln(e.Out, "Successfully configured the given webhooks for the app.") + return nil +} + func newConfigureCmd(e *env) *cobra.Command { var c configureCmd @@ -225,5 +576,16 @@ func newConfigureCmd(e *env) *cobra.Command { } cmd.AddCommand(projectCmd) + hooksCmd := &cobra.Command{ + Use: "hooks [filename]", + Short: "Configure webhooks according to given config file", + Long: "Configure webhooks for the app based on the given configuration csv file.", + Run: runWithArgsClient(e, c.hooksCmd), + Aliases: []string{"webhooks"}, + } + hooksCmd.Flags().BoolVarP(&c.hooksStrict, "strict", "s", c.hooksStrict, + "Configure hooks in strict mode, i.e., do not automatically fix errors.") + cmd.AddCommand(hooksCmd) + return cmd } diff --git a/configure_cmd_test.go b/configure_cmd_test.go index 43e4547..37aeeeb 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -2,6 +2,7 @@ package main import ( "io/ioutil" + "os" "path/filepath" "regexp" "strings" @@ -150,3 +151,363 @@ func TestProjectType(t *testing.T) { ensure.StringContains(t, h.Out.String(), "Successfully set project type to: parse") ensure.Nil(t, err) } + +func TestCheckTriggerName(t *testing.T) { + t.Parallel() + + c := &configureCmd{} + + ensure.Nil(t, c.checkTriggerName("beforeSave")) + ensure.Nil(t, c.checkTriggerName("afterSave")) + ensure.Nil(t, c.checkTriggerName("beforeDelete")) + ensure.Nil(t, c.checkTriggerName("afterDelete")) + + ensure.Nil(t, c.checkTriggerName("BeforeSAVE")) + ensure.Nil(t, c.checkTriggerName("AFTERsave")) + ensure.Nil(t, c.checkTriggerName("BeforeDELETE")) + ensure.Nil(t, c.checkTriggerName("AFTERdelete")) + + ensure.Err(t, c.checkTriggerName("invalid"), regexp.MustCompile("list of valid trigger names")) +} + +func TestPostOrPutHook(t *testing.T) { + t.Parallel() + h := newHarness(t) + defer h.Stop() + + c := &configureCmd{} + + _, _, err := c.postOrPutHook(h.env, nil, "other") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, _, err = c.postOrPutHook(h.env, nil, "post", "1") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, _, err = c.postOrPutHook(h.env, nil, "put", "1", "2", "3", "4") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + var hooksOps []*hookOperation + _, ops, err := c.postOrPutHook(h.env, hooksOps, + "post", "call", "https://api.twilio.com/call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{ + FunctionName: "call", + URL: "https://api.twilio.com/call", + }, + ) + + _, ops, err = c.postOrPutHook(h.env, hooksOps, + "put", "call", "https://api.twilio.com/call_1") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{ + FunctionName: "call", + URL: "https://api.twilio.com/call_1", + }, + ) + + _, ops, err = c.postOrPutHook(h.env, hooksOps, "post", + "_User", "beforeSave", "https://api.twilio.com/message") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ + ClassName: "_User", + TriggerName: "beforeSave", + URL: "https://api.twilio.com/message", + }, + ) + + _, ops, err = c.postOrPutHook(h.env, hooksOps, "put", + "_User", "beforeSave", "https://api.twilio.com/message_1") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ + ClassName: "_User", + TriggerName: "beforeSave", + URL: "https://api.twilio.com/message_1", + }, + ) + + _, ops, err = c.postOrPutHook(h.env, hooksOps, "post", + "_User", "invalid", "https://other") + ensure.Err(t, err, regexp.MustCompile("invalid trigger")) +} + +func TestDeleteHook(t *testing.T) { + t.Parallel() + h := newHarness(t) + defer h.Stop() + + c := &configureCmd{} + _, _, err := c.deleteHook(h.env, nil, "delete") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, _, err = c.deleteHook(h.env, nil, "invalid", "1") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, _, err = c.deleteHook(h.env, nil, "delete", "1", "2") + ensure.Err(t, err, regexp.MustCompile("invalid trigger name")) + + var hooksOps []*hookOperation + _, ops, err := c.deleteHook(h.env, hooksOps, "delete", "call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{FunctionName: "call"}, + ) + + _, ops, err = c.deleteHook(h.env, hooksOps, "delete", "_User", "beforeSave") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ClassName: "_User", TriggerName: "beforeSave"}, + ) +} + +func TestAppendHookOperation(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + c := &configureCmd{} + + var hooksOps []*hookOperation + _, ops, err := c.appendHookOperation(h.env, "\n\t ", hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, hooksOps, ops) + + _, _, err = c.appendHookOperation(h.env, "invalid", nil) + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, ops, err = c.appendHookOperation(h.env, "post,call,https://twilio.com/call", hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{FunctionName: "call", URL: "https://twilio.com/call"}, + ) + + _, ops, err = c.appendHookOperation(h.env, "put,call,https://twilio.com/call_1", hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{FunctionName: "call", URL: "https://twilio.com/call_1"}, + ) + + _, ops, err = c.appendHookOperation(h.env, "put,call,https://twilio.com/call_1,call_2", hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{FunctionName: "call", URL: "https://twilio.com/call_1,call_2"}, + ) + + //random junk + _, ops, err = c.appendHookOperation(h.env, + "put,call,https://twilio.com/call_1,call_2,call_3", hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{FunctionName: "call", URL: "https://twilio.com/call_1,call_2,call_3"}, + ) + + _, ops, err = c.appendHookOperation(h.env, + "posT,_User,afterDelete,https://twilio.com/message", hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message"}, + ) + + _, ops, err = c.appendHookOperation(h.env, + "pUt,_User,afterDelete,https://twilio.com/message_1", hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1"}, + ) + + // random junk + _, ops, err = c.appendHookOperation(h.env, + "pUt,_User,afterDelete,https://twilio.com/message_1,message_2", hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1,message_2"}, + ) +} + +func TestCheckStrictMode(t *testing.T) { + t.Parallel() + + testCases := []struct { + restOp string + exists bool + strictMode bool + + op string + updated bool + err *regexp.Regexp + }{ + {exists: false, restOp: "put", strictMode: false, op: "POST", updated: true, err: nil}, + {exists: false, restOp: "post", strictMode: false, op: "POST", updated: false, err: nil}, + {exists: false, restOp: "delete", strictMode: false, op: "DELETE", updated: true, err: nil}, + {exists: true, restOp: "put", strictMode: false, op: "PUT", updated: false, err: nil}, + {exists: true, restOp: "post", strictMode: false, op: "PUT", updated: true, err: nil}, + {exists: true, restOp: "delete", strictMode: false, op: "DELETE", updated: false, err: nil}, + + {exists: false, restOp: "put", strictMode: true, err: regexp.MustCompile("does not exist")}, + {exists: false, restOp: "post", strictMode: true, op: "POST", updated: false, err: nil}, + {exists: false, restOp: "delete", strictMode: true, err: regexp.MustCompile("cannot delete")}, + {exists: true, restOp: "put", strictMode: true, op: "PUT", updated: false, err: nil}, + {exists: true, restOp: "post", strictMode: true, err: regexp.MustCompile("already exists")}, + {exists: true, restOp: "delete", strictMode: true, op: "DELETE", updated: false, err: nil}, + } + + c := &configureCmd{} + for _, testCase := range testCases { + c.hooksStrict = testCase.strictMode + op, updated, err := c.checkStrictMode(testCase.restOp, testCase.exists) + if testCase.err == nil { + ensure.Nil(t, err) + ensure.DeepEqual(t, op, testCase.op) + ensure.DeepEqual(t, updated, testCase.updated) + } else { + ensure.Err(t, err, testCase.err) + } + } +} + +func TestCreateHookOperations(t *testing.T) { + t.Parallel() + h := newHarness(t) + h.makeEmptyRoot() + defer h.Stop() + + hooksConfigFile := filepath.Join(h.env.Root, "webhooks.csv") + err := ioutil.WriteFile( + hooksConfigFile, + []byte{}, + 0666, + ) + ensure.Nil(t, err) + + c := &configureCmd{} + + f, err := os.Open(hooksConfigFile) + ensure.Nil(t, err) + ops, err := c.createHooksOperations(h.env, f) + ensure.Nil(t, err) + ensure.DeepEqual(t, len(ops), 0) + ensure.Nil(t, f.Close()) + + err = ioutil.WriteFile( + hooksConfigFile, + []byte("\n"), + 0666, + ) + + f, err = os.Open(hooksConfigFile) + ensure.Nil(t, err) + ops, err = c.createHooksOperations(h.env, f) + ensure.Nil(t, err) + ensure.DeepEqual(t, len(ops), 0) + ensure.DeepEqual(t, h.Out.String(), "Ignoring line: 1\n") + ensure.Nil(t, f.Close()) + h.Out.Reset() +} + +func TestFunctionHookExists(t *testing.T) { + t.Parallel() + h := newFunctionsHarness(t) + c := &configureCmd{} + exists, err := c.functionHookExists(h.env, "foo") + ensure.Nil(t, err) + ensure.True(t, exists) + + exists, err = c.functionHookExists(h.env, "bar") + ensure.Nil(t, err) + ensure.False(t, exists) +} + +func TestDeployFunctionHooks(t *testing.T) { + t.Parallel() + h := newFunctionsHarness(t) + c := &configureCmd{} + + err := c.deployFunctionHook( + h.env, + &hookOperation{ + method: "post", + function: &functionHook{ + FunctionName: "foo", + URL: "https://api.example.com/foo", + }, + }, + ) + // not an error -> will be converted to put + ensure.Nil(t, err) +} + +func TestTriggerHookExists(t *testing.T) { + t.Parallel() + h := newTriggersHarness(t) + c := &configureCmd{} + exists, err := c.triggerHookExists(h.env, "foo", "beforeSave") + ensure.Nil(t, err) + ensure.True(t, exists) + + exists, err = c.triggerHookExists(h.env, "bar", "other") + ensure.Nil(t, err) + ensure.False(t, exists) +} + +func TestDeployTriggerHooks(t *testing.T) { + t.Parallel() + h := newTriggersHarness(t) + c := &configureCmd{} + + err := c.deployTriggerHook( + h.env, + &hookOperation{ + method: "post", + trigger: &triggerHook{ + ClassName: "foo", + TriggerName: "beforeSave", + URL: "https://api.example.com/foo", + }, + }, + ) + // not an error -> will be converted to put + ensure.Nil(t, err) +} diff --git a/functions_cmd.go b/functions_cmd.go index 14dfda4..9cb5a45 100644 --- a/functions_cmd.go +++ b/functions_cmd.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -25,11 +26,16 @@ func (f functionHook) String() string { } type functionHooksCmd struct { - All bool - Function *functionHook + All bool + Function *functionHook + interactive bool } -func readFunctionName(e *env) (*functionHook, error) { +func readFunctionName(e *env, params *functionHook) (*functionHook, error) { + if params != nil && params.FunctionName != "" { + return params, nil + } + var f functionHook fmt.Fprintf(e.Out, "Please enter the function name: ") fmt.Fscanf(e.In, "%s\n", &f.FunctionName) @@ -39,8 +45,12 @@ func readFunctionName(e *env) (*functionHook, error) { return &f, nil } -func readFunctionParams(e *env) (*functionHook, error) { - f, err := readFunctionName(e) +func readFunctionParams(e *env, params *functionHook) (*functionHook, error) { + if params != nil && params.FunctionName != "" && params.URL != "" { + return params, nil + } + + f, err := readFunctionName(e, params) if err != nil { return nil, err } @@ -58,18 +68,19 @@ func readFunctionParams(e *env) (*functionHook, error) { const defaultFunctionsURL = "/1/hooks/functions" func (h *functionHooksCmd) functionHooksCreate(e *env, ctx *context) error { - params, err := readFunctionParams(e) + params, err := readFunctionParams(e, h.Function) if err != nil { return err } + var res functionHook functionsURL, err := url.Parse(defaultFunctionsURL) if err != nil { - return err + return stackerr.Wrap(err) } _, err = e.ParseAPIClient.Post(functionsURL, params, &res) if err != nil { - return err + return stackerr.Wrap(err) } if res.Warning != "" { fmt.Fprintf(e.Err, "WARNING: %s\n", res.Warning) @@ -87,7 +98,7 @@ func (h *functionHooksCmd) functionHooksRead(e *env, ctx *context) error { u := defaultFunctionsURL var function *functionHook if !h.All { - funct, err := readFunctionName(e) + funct, err := readFunctionName(e, h.Function) if err != nil { return err } @@ -96,7 +107,7 @@ func (h *functionHooksCmd) functionHooksRead(e *env, ctx *context) error { } functionsURL, err := url.Parse(u) if err != nil { - return err + return stackerr.Wrap(err) } var res struct { @@ -104,7 +115,7 @@ func (h *functionHooksCmd) functionHooksRead(e *env, ctx *context) error { } _, err = e.ParseAPIClient.Get(functionsURL, &res) if err != nil { - return err + return stackerr.Wrap(err) } var output []string for _, function := range res.Results { @@ -126,19 +137,19 @@ func (h *functionHooksCmd) functionHooksRead(e *env, ctx *context) error { } func (h *functionHooksCmd) functionHooksUpdate(e *env, ctx *context) error { - params, err := readFunctionParams(e) + params, err := readFunctionParams(e, h.Function) if err != nil { return err } var res functionHook functionsURL, err := url.Parse(path.Join(defaultFunctionsURL, params.FunctionName)) if err != nil { - return err + return stackerr.Wrap(err) } _, err = e.ParseAPIClient.Put(functionsURL, &functionHook{URL: params.URL}, &res) if err != nil { - return err + return stackerr.Wrap(err) } if res.Warning != "" { fmt.Fprintf(e.Err, "WARNING: %s\n", res.Warning) @@ -153,13 +164,13 @@ func (h *functionHooksCmd) functionHooksUpdate(e *env, ctx *context) error { } func (h *functionHooksCmd) functionHooksDelete(e *env, ctx *context) error { - params, err := readFunctionName(e) + params, err := readFunctionName(e, h.Function) if err != nil { return err } functionsURL, err := url.Parse(path.Join(defaultFunctionsURL, params.FunctionName)) if err != nil { - return err + return stackerr.Wrap(err) } confirmMessage := fmt.Sprintf( @@ -168,10 +179,10 @@ func (h *functionHooksCmd) functionHooksDelete(e *env, ctx *context) error { ) var res functionHook - if getConfirmation(confirmMessage, e) { + if !h.interactive || getConfirmation(confirmMessage, e) { _, err = e.ParseAPIClient.Put(functionsURL, map[string]interface{}{"__op": "Delete"}, &res) if err != nil { - return err + return stackerr.Wrap(err) } fmt.Fprintf(e.Out, "Successfully deleted webhook function %q\n", params.FunctionName) if res.FunctionName != "" { @@ -189,7 +200,7 @@ func (h *functionHooksCmd) functionHooks(e *env, c *context) error { } func newFunctionHooksCmd(e *env) *cobra.Command { - var h functionHooksCmd + h := &functionHooksCmd{interactive: true} c := &cobra.Command{ Use: "functions", diff --git a/functions_cmd_test.go b/functions_cmd_test.go index 24a9b2e..4823c3d 100644 --- a/functions_cmd_test.go +++ b/functions_cmd_test.go @@ -37,8 +37,7 @@ func newFunctionsHarness(t testing.TB) *Harness { {"functionName": "foo", "url": "https://api.example.com/foo"}, }, } - } else { - ensure.DeepEqual(t, r.URL.Path, defaultFunctionsURL) + } else if r.URL.Path == defaultFunctionsURL { body = map[string]interface{}{ "results": []map[string]interface{}{ {"functionName": "foo"}, @@ -46,6 +45,10 @@ func newFunctionsHarness(t testing.TB) *Harness { {"functionName": "bar", "url": "https://api.example.com/bar"}, }, } + } else { + return &http.Response{StatusCode: http.StatusBadRequest}, + errors.New("no such function hook is defined") + } case "POST": ensure.DeepEqual(t, r.URL.Path, defaultFunctionsURL) @@ -117,16 +120,16 @@ func TestReadFuncParams(t *testing.T) { defer h.Stop() h.env.In = strings.NewReader("\n") - _, err := readFunctionName(h.env) + _, err := readFunctionName(h.env, nil) ensure.Err(t, err, regexp.MustCompile("Function name cannot be empty")) h.env.In = strings.NewReader("foo\n") - hook, err := readFunctionName(h.env) + hook, err := readFunctionName(h.env, nil) ensure.Nil(t, err) ensure.DeepEqual(t, *hook, functionHook{FunctionName: "foo"}) h.env.In = strings.NewReader("foo\napi.example.com/foo\n") - hook, err = readFunctionParams(h.env) + hook, err = readFunctionParams(h.env, nil) ensure.Nil(t, err) ensure.DeepEqual(t, *hook, functionHook{ FunctionName: "foo", @@ -216,7 +219,7 @@ func TestFunctionHookDelete(t *testing.T) { h := newFunctionsHarness(t) - var f functionHooksCmd + f := &functionHooksCmd{interactive: true} h.env.In = strings.NewReader("foo\ny\n") ensure.Nil(t, f.functionHooksDelete(h.env, nil)) ensure.DeepEqual(t, diff --git a/triggers_cmd.go b/triggers_cmd.go index 86942cd..0845ba6 100644 --- a/triggers_cmd.go +++ b/triggers_cmd.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -26,11 +27,16 @@ func (t *triggerHook) String() string { } type triggerHooksCmd struct { - All bool - Trigger *triggerHook + All bool + Trigger *triggerHook + interactive bool } -func readTriggerName(e *env) (*triggerHook, error) { +func readTriggerName(e *env, params *triggerHook) (*triggerHook, error) { + if params != nil && params.ClassName != "" && params.TriggerName != "" { + return params, nil + } + var t triggerHook fmt.Fprintln(e.Out, "Please enter following details about the trigger webhook") fmt.Fprint(e.Out, "Class name: ") @@ -46,8 +52,12 @@ func readTriggerName(e *env) (*triggerHook, error) { return &t, nil } -func readTriggerParams(e *env) (*triggerHook, error) { - t, err := readTriggerName(e) +func readTriggerParams(e *env, params *triggerHook) (*triggerHook, error) { + if params != nil && params.ClassName != "" && params.TriggerName != "" && params.URL != "" { + return params, nil + } + + t, err := readTriggerName(e, params) if err != nil { return nil, err } @@ -63,18 +73,18 @@ func readTriggerParams(e *env) (*triggerHook, error) { const defaultTriggersURL = "/1/hooks/triggers" func (h *triggerHooksCmd) triggerHooksCreate(e *env, ctx *context) error { - params, err := readTriggerParams(e) + params, err := readTriggerParams(e, h.Trigger) if err != nil { return err } var res triggerHook triggersURL, err := url.Parse(defaultTriggersURL) if err != nil { - return err + return stackerr.Wrap(err) } _, err = e.ParseAPIClient.Post(triggersURL, params, &res) if err != nil { - return err + return stackerr.Wrap(err) } if res.Warning != "" { fmt.Fprintf(e.Err, "WARNING: %s\n", res.Warning) @@ -92,7 +102,7 @@ func (h *triggerHooksCmd) triggerHooksRead(e *env, ctx *context) error { u := defaultTriggersURL var trigger *triggerHook if !h.All { - trig, err := readTriggerName(e) + trig, err := readTriggerName(e, h.Trigger) if err != nil { return err } @@ -101,14 +111,14 @@ func (h *triggerHooksCmd) triggerHooksRead(e *env, ctx *context) error { } triggersURL, err := url.Parse(u) if err != nil { - return err + return stackerr.Wrap(err) } var res struct { Results []*triggerHook `json:"results,omitempty"` } _, err = e.ParseAPIClient.Get(triggersURL, &res) if err != nil { - return err + return stackerr.Wrap(err) } var output []string for _, trigger := range res.Results { @@ -131,18 +141,18 @@ func (h *triggerHooksCmd) triggerHooksRead(e *env, ctx *context) error { } func (h *triggerHooksCmd) triggerHooksUpdate(e *env, ctx *context) error { - params, err := readTriggerParams(e) + params, err := readTriggerParams(e, h.Trigger) if err != nil { return err } var res triggerHook triggersURL, err := url.Parse(path.Join(defaultTriggersURL, params.ClassName, params.TriggerName)) if err != nil { - return err + return stackerr.Wrap(err) } _, err = e.ParseAPIClient.Put(triggersURL, &triggerHook{URL: params.URL}, &res) if err != nil { - return err + return stackerr.Wrap(err) } if res.Warning != "" { fmt.Fprintf(e.Err, "WARNING: %s\n", res.Warning) @@ -158,13 +168,13 @@ func (h *triggerHooksCmd) triggerHooksUpdate(e *env, ctx *context) error { } func (h *triggerHooksCmd) triggerHooksDelete(e *env, ctx *context) error { - params, err := readTriggerName(e) + params, err := readTriggerName(e, h.Trigger) if err != nil { return err } triggersURL, err := url.Parse(path.Join(defaultTriggersURL, params.ClassName, params.TriggerName)) if err != nil { - return err + return stackerr.Wrap(err) } confirmMessage := fmt.Sprintf("Are you sure you want to delete %q webhook trigger for class: %q (y/n): ", @@ -173,10 +183,10 @@ func (h *triggerHooksCmd) triggerHooksDelete(e *env, ctx *context) error { ) var res triggerHook - if getConfirmation(confirmMessage, e) { + if !h.interactive || getConfirmation(confirmMessage, e) { _, err = e.ParseAPIClient.Put(triggersURL, map[string]interface{}{"__op": "Delete"}, &res) if err != nil { - return err + return stackerr.Wrap(err) } fmt.Fprintf(e.Out, "Successfully deleted %q webhook trigger for class %q\n", params.TriggerName, @@ -199,7 +209,7 @@ func (h *triggerHooksCmd) triggerHooks(e *env, c *context) error { } func newTriggerHooksCmd(e *env) *cobra.Command { - var h triggerHooksCmd + h := &triggerHooksCmd{interactive: true} c := &cobra.Command{ Use: "triggers", diff --git a/triggers_cmd_test.go b/triggers_cmd_test.go index c06bc7b..dca461d 100644 --- a/triggers_cmd_test.go +++ b/triggers_cmd_test.go @@ -38,8 +38,7 @@ func newTriggersHarness(t testing.TB) *Harness { {"className": "foo", "triggerName": "beforeSave", "url": "https://api.example.com/foo/beforeSave"}, }, } - } else { - ensure.DeepEqual(t, r.URL.Path, defaultTriggersURL) + } else if r.URL.Path == defaultTriggersURL { body = map[string]interface{}{ "results": []map[string]interface{}{ {"className": "foo", "triggerName": "beforeSave"}, @@ -47,6 +46,9 @@ func newTriggersHarness(t testing.TB) *Harness { {"className": "bar", "triggerName": "afterSave", "url": "https://api.example.com/bar/afterSave"}, }, } + } else { + return &http.Response{StatusCode: http.StatusBadRequest}, + errors.New("no such trigger is defined") } case "POST": ensure.DeepEqual(t, r.URL.Path, defaultTriggersURL) @@ -122,20 +124,20 @@ func TestReadTriggerParams(t *testing.T) { defer h.Stop() h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - _, err := readTriggerName(h.env) + _, err := readTriggerName(h.env, nil) ensure.Err(t, err, regexp.MustCompile("Class name cannot be empty")) h.env.In = ioutil.NopCloser(strings.NewReader("foo\n")) - _, err = readTriggerName(h.env) + _, err = readTriggerName(h.env, nil) ensure.Err(t, err, regexp.MustCompile("Trigger name cannot be empty")) h.env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave")) - hook, err := readTriggerName(h.env) + hook, err := readTriggerName(h.env, nil) ensure.Nil(t, err) ensure.DeepEqual(t, *hook, triggerHook{ClassName: "foo", TriggerName: "beforeSave"}) h.env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\napi.example.com/foo/beforeSave\n")) - hook, err = readTriggerParams(h.env) + hook, err = readTriggerParams(h.env, nil) ensure.Nil(t, err) ensure.DeepEqual(t, *hook, triggerHook{ ClassName: "foo", @@ -234,7 +236,7 @@ func TestTriggerHookDelete(t *testing.T) { h := newTriggersHarness(t) - var tr triggerHooksCmd + tr := triggerHooksCmd{interactive: true} h.env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\ny\n")) ensure.Nil(t, tr.triggerHooksDelete(h.env, nil)) ensure.DeepEqual(t, From ebb485db0b28f07c9487e861b583ab2dfee3b2b0 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 22 Sep 2015 22:02:38 +0000 Subject: [PATCH 053/106] [new] allow parse init of current directory --- new.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/new.go b/new.go index 62a921b..0b3625b 100644 --- a/new.go +++ b/new.go @@ -205,6 +205,8 @@ Please refrain from creating a Parse project inside another Parse project. if nonInteractive { cloudCodeDir = n.codeLocation + } else if n.noCode { + cloudCodeDir = "" // ensures that "parse new --init" inits the current directory } else { cloudCodeDir, err = n.getCloudCodeDir(e, name, isNew) if err != nil { From fb8497824290083382fcd69a56343bb569f666cf Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 23 Sep 2015 16:43:08 +0000 Subject: [PATCH 054/106] [configure hooks] refactor configure hooks command * refactor configure hooks command so line processing is more modular --- configure_cmd.go | 46 +++++++++++++++++++-------- configure_cmd_test.go | 74 +++++++++++++++++++++++++++---------------- 2 files changed, 78 insertions(+), 42 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index b5b616e..ea166ca 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -302,23 +302,15 @@ func (c *configureCmd) deleteHook( func (c *configureCmd) appendHookOperation( e *env, - op string, + fields []string, hooks []*hookOperation, ) (bool, []*hookOperation, error) { - op = strings.TrimSpace(op) - if op == "" { + if len(fields) == 0 { return false, hooks, nil } - fields := strings.SplitN(op, ",", 4) - restOp := strings.ToLower(fields[0]) - - switch restOp { + switch strings.ToLower(fields[0]) { case "post", "put": - if len(fields) > 2 && strings.HasPrefix(fields[2], "https://") { - fields[2] = strings.Join(fields[2:], ",") - fields = fields[:3] - } return c.postOrPutHook(e, hooks, fields...) case "delete": @@ -327,6 +319,30 @@ func (c *configureCmd) appendHookOperation( return false, nil, stackerr.Wrap(errInvalidFormat) } +func (c *configureCmd) processHooksOperation(e *env, op string) ([]string, error) { + op = strings.TrimSpace(op) + if op == "" { + return nil, nil + } + fields := strings.SplitN(op, ",", 4) + if restOp := strings.ToLower(fields[0]); restOp != "post" && restOp != "put" { + return fields, nil + } + if len(fields) < 3 { + return nil, stackerr.Wrap(errInvalidFormat) + } + + switch e.Type { + case legacyParseFormat, parseFormat: + if strings.HasPrefix(fields[2], "https://") { + fields[2] = strings.Join(fields[2:], ",") + fields = fields[:3] + } + return fields, nil + } + return nil, stackerr.Wrap(errInvalidFormat) +} + func (c *configureCmd) createHooksOperations( e *env, reader io.Reader, @@ -335,13 +351,15 @@ func (c *configureCmd) createHooksOperations( var ( hooksOps []*hookOperation added bool - err error ) lineNum := 0 for scanner.Scan() { lineNum++ - line := scanner.Text() - added, hooksOps, err = c.appendHookOperation(e, line, hooksOps) + fields, err := c.processHooksOperation(e, scanner.Text()) + if err != nil { + return nil, err + } + added, hooksOps, err = c.appendHookOperation(e, fields, hooksOps) if err != nil { return nil, err } diff --git a/configure_cmd_test.go b/configure_cmd_test.go index 37aeeeb..f1ffdbe 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -281,6 +281,41 @@ func TestDeleteHook(t *testing.T) { ) } +func TestProcessHookOperation(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + c := &configureCmd{} + + ops, err := c.processHooksOperation(h.env, "\n\t ") + ensure.Nil(t, err) + ensure.DeepEqual(t, len(ops), 0) + + ops, err = c.processHooksOperation(h.env, "invalid") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"invalid"}) + + ops, err = c.processHooksOperation(h.env, "post,call,https://twilio.com/call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) + + ops, err = c.processHooksOperation(h.env, "put,call,https://twilio.com/call_1,call_2") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2"}) + + ops, err = c.processHooksOperation(h.env, "put,call,https://twilio.com/call_1,call_2,call_3") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) + + ops, err = c.processHooksOperation(h.env, + "pUt,_User,afterDelete,https://twilio.com/message_1,message_2") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"pUt", "_User", "afterDelete", + "https://twilio.com/message_1,message_2"}) +} + func TestAppendHookOperation(t *testing.T) { t.Parallel() @@ -290,14 +325,15 @@ func TestAppendHookOperation(t *testing.T) { c := &configureCmd{} var hooksOps []*hookOperation - _, ops, err := c.appendHookOperation(h.env, "\n\t ", hooksOps) + _, ops, err := c.appendHookOperation(h.env, nil, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, hooksOps, ops) - _, _, err = c.appendHookOperation(h.env, "invalid", nil) + _, _, err = c.appendHookOperation(h.env, []string{"invalid"}, nil) ensure.Err(t, err, regexp.MustCompile("invalid format")) - _, ops, err = c.appendHookOperation(h.env, "post,call,https://twilio.com/call", hooksOps) + _, ops, err = c.appendHookOperation(h.env, + []string{"post", "call", "https://twilio.com/call"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") ensure.DeepEqual( @@ -306,37 +342,19 @@ func TestAppendHookOperation(t *testing.T) { functionHook{FunctionName: "call", URL: "https://twilio.com/call"}, ) - _, ops, err = c.appendHookOperation(h.env, "put,call,https://twilio.com/call_1", hooksOps) - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{FunctionName: "call", URL: "https://twilio.com/call_1"}, - ) - - _, ops, err = c.appendHookOperation(h.env, "put,call,https://twilio.com/call_1,call_2", hooksOps) - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{FunctionName: "call", URL: "https://twilio.com/call_1,call_2"}, - ) - - //random junk _, ops, err = c.appendHookOperation(h.env, - "put,call,https://twilio.com/call_1,call_2,call_3", hooksOps) + []string{"put", "call", "https://twilio.com/call_1"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") ensure.DeepEqual( t, *ops[len(ops)-1].function, - functionHook{FunctionName: "call", URL: "https://twilio.com/call_1,call_2,call_3"}, + functionHook{FunctionName: "call", URL: "https://twilio.com/call_1"}, ) + //random stuff _, ops, err = c.appendHookOperation(h.env, - "posT,_User,afterDelete,https://twilio.com/message", hooksOps) + []string{"posT", "_User", "afterDelete", "https://twilio.com/message"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") ensure.DeepEqual( @@ -346,7 +364,7 @@ func TestAppendHookOperation(t *testing.T) { ) _, ops, err = c.appendHookOperation(h.env, - "pUt,_User,afterDelete,https://twilio.com/message_1", hooksOps) + []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") ensure.DeepEqual( @@ -355,9 +373,9 @@ func TestAppendHookOperation(t *testing.T) { triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1"}, ) - // random junk + // random stuff _, ops, err = c.appendHookOperation(h.env, - "pUt,_User,afterDelete,https://twilio.com/message_1,message_2", hooksOps) + []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1,message_2"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") ensure.DeepEqual( From 0eaa5fce7e86452b165328042f7d63e1a6ed83c4 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 23 Sep 2015 18:05:15 +0000 Subject: [PATCH 055/106] [configure hooks] resolve base url if provided if a base reference url is provided use it to resolve the relative urls provided. this helps eliminate the need to type the base url repeatedly --- configure_cmd.go | 51 +++++++++++++++++++++++++++++++++++++------ configure_cmd_test.go | 38 +++++++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index ea166ca..181beb3 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -43,7 +43,11 @@ type configureCmd struct { login login isDefault bool tokenReader io.Reader // for testing - hooksStrict bool + + // for hooks sub-command + hooksStrict bool + baseURL string + baseWebhookURL *url.URL } func (c *configureCmd) accountKey(e *env) error { @@ -324,7 +328,8 @@ func (c *configureCmd) processHooksOperation(e *env, op string) ([]string, error if op == "" { return nil, nil } - fields := strings.SplitN(op, ",", 4) + + fields := strings.SplitN(op, ",", 3) if restOp := strings.ToLower(fields[0]); restOp != "post" && restOp != "put" { return fields, nil } @@ -332,12 +337,22 @@ func (c *configureCmd) processHooksOperation(e *env, op string) ([]string, error return nil, stackerr.Wrap(errInvalidFormat) } - switch e.Type { - case legacyParseFormat, parseFormat: - if strings.HasPrefix(fields[2], "https://") { - fields[2] = strings.Join(fields[2:], ",") - fields = fields[:3] + if c.baseWebhookURL != nil { + u, err := c.baseWebhookURL.Parse(fields[2]) + if err != nil { + return nil, stackerr.Wrap(err) } + fields[2] = u.String() + } + + switch subFields := strings.SplitN(fields[1], ":", 2); len(subFields) { + case 1: + return fields, nil + case 2: + fields = append(fields, fields[2]) + fields[3] = fields[2] + fields[2] = subFields[1] + fields[1] = subFields[0] return fields, nil } return nil, stackerr.Wrap(errInvalidFormat) @@ -524,10 +539,29 @@ func (c *configureCmd) deployWebhooksConfig(e *env, hooksOps []*hookOperation) e return nil } +func (c *configureCmd) parseBaseURL(e *env) error { + if c.baseURL != "" { + u, err := url.Parse(c.baseURL) + if err != nil { + fmt.Fprintln(e.Err, "Invalid base webhook url provided") + return stackerr.Wrap(err) + } + if u.Scheme != "https" { + return stackerr.New("Please provide a valid https url") + } + c.baseWebhookURL = u + } + return nil +} + func (c *configureCmd) hooksCmd(e *env, ctx *context, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid args: %v, only an optional hooks config file is expected.", args) } + if err := c.parseBaseURL(e); err != nil { + return err + } + reader := e.In if len(args) == 1 { file, err := os.Open(args[0]) @@ -603,6 +637,9 @@ func newConfigureCmd(e *env) *cobra.Command { } hooksCmd.Flags().BoolVarP(&c.hooksStrict, "strict", "s", c.hooksStrict, "Configure hooks in strict mode, i.e., do not automatically fix errors.") + hooksCmd.Flags().StringVarP(&c.baseURL, "base", "b", c.baseURL, + `Base url to use while parsing the webhook url field. +If provided, the config file can have relative urls.`) cmd.AddCommand(hooksCmd) return cmd diff --git a/configure_cmd_test.go b/configure_cmd_test.go index f1ffdbe..54b0e07 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -2,6 +2,7 @@ package main import ( "io/ioutil" + "net/url" "os" "path/filepath" "regexp" @@ -310,10 +311,30 @@ func TestProcessHookOperation(t *testing.T) { ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) ops, err = c.processHooksOperation(h.env, - "pUt,_User,afterDelete,https://twilio.com/message_1,message_2") + "pUt,_User:afterDelete,https://twilio.com/message_1,message_2") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1,message_2"}) + + u, err := url.Parse("https://parse.com") + ensure.Nil(t, err) + c.baseWebhookURL = u + + ops, err = c.processHooksOperation(h.env, "post,call,https://parse.com/call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"post", "call", "https://parse.com/call"}) + + ops, err = c.processHooksOperation(h.env, "post,call,https://twilio.com/call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) + + ops, err = c.processHooksOperation(h.env, "put,call,/call_1,call_2,call_3") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"put", "call", "https://parse.com/call_1,call_2,call_3"}) + + ops, err = c.processHooksOperation(h.env, "put,call,https://twilio.com/call_1,call_2,call_3") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) } func TestAppendHookOperation(t *testing.T) { @@ -529,3 +550,18 @@ func TestDeployTriggerHooks(t *testing.T) { // not an error -> will be converted to put ensure.Nil(t, err) } + +func TestParseBaseURL(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + c := &configureCmd{} + c.baseURL = "http://hello" + ensure.Err(t, c.parseBaseURL(h.env), regexp.MustCompile("valid https url")) + + c.baseURL = "https://hello" + ensure.Nil(t, c.parseBaseURL(h.env)) + ensure.DeepEqual(t, c.baseWebhookURL.String(), c.baseURL) +} From 947728221156d54908151c5e8e63ba6864218e79 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 23 Sep 2015 21:10:55 +0000 Subject: [PATCH 056/106] [configure hooks] fix line processing for delete fix the bug in line processing for deleting a trigger also fix the error message for an invalid format --- configure_cmd.go | 27 +++++++++++++++++++++------ configure_cmd_test.go | 14 ++++++++++++-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/configure_cmd.go b/configure_cmd.go index 181beb3..dbb4f53 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -19,11 +19,14 @@ import ( var ( errInvalidFormat = errors.New( - `invalid format. valid formats should look like: + ` +invalid format. +valid formats should look like: [put|post],functionName,https_url delete,functionName -[put|post],className,triggerName,https_url -delete,className,triggerName + +[put|post],className:triggerName,https_url +delete,className:triggerName `) errPostToPut = errors.New( @@ -330,10 +333,22 @@ func (c *configureCmd) processHooksOperation(e *env, op string) ([]string, error } fields := strings.SplitN(op, ",", 3) - if restOp := strings.ToLower(fields[0]); restOp != "post" && restOp != "put" { + switch restOp := strings.ToLower(fields[0]); restOp { + case "post", "put": + if len(fields) < 3 { + return nil, stackerr.Wrap(errInvalidFormat) + } + case "delete": + if len(fields) != 2 { + return nil, stackerr.Wrap(errInvalidFormat) + } + subFields := strings.SplitN(fields[1], ":", 2) + if len(subFields) == 2 { + fields = append(fields, subFields[1]) + fields[1] = subFields[0] + } return fields, nil - } - if len(fields) < 3 { + default: return nil, stackerr.Wrap(errInvalidFormat) } diff --git a/configure_cmd_test.go b/configure_cmd_test.go index 54b0e07..db0107d 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -294,9 +294,19 @@ func TestProcessHookOperation(t *testing.T) { ensure.Nil(t, err) ensure.DeepEqual(t, len(ops), 0) - ops, err = c.processHooksOperation(h.env, "invalid") + _, err = c.processHooksOperation(h.env, "invalid") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, err = c.processHooksOperation(h.env, "delete,call,caller") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + ops, err = c.processHooksOperation(h.env, "delete,call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"delete", "call"}) + + ops, err = c.processHooksOperation(h.env, "delete,_User:beforeSave") ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"invalid"}) + ensure.DeepEqual(t, ops, []string{"delete", "_User", "beforeSave"}) ops, err = c.processHooksOperation(h.env, "post,call,https://twilio.com/call") ensure.Nil(t, err) From 68626553e631976af713d75274d5c1c7dd01ad1d Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 23 Sep 2015 21:45:01 -0700 Subject: [PATCH 057/106] [cli-tests] add more tests to improve code coverage --- new.go | 2 +- new_cmd_test.go | 111 +++++++++++++++++++++++++++++++++++++++++++++ update_cmd.go | 16 +++++-- update_cmd_test.go | 4 ++ 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/new.go b/new.go index 0b3625b..eabf6e4 100644 --- a/new.go +++ b/new.go @@ -78,7 +78,7 @@ Directory Name: `, ) } - fmt.Scanf("%s\n", &cloudCodeDir) + fmt.Fscanf(e.In, "%s\n", &cloudCodeDir) cloudCodeDir = strings.TrimSpace(cloudCodeDir) if cloudCodeDir == "" { cloudCodeDir = appName diff --git a/new_cmd_test.go b/new_cmd_test.go index 663b811..002b262 100644 --- a/new_cmd_test.go +++ b/new_cmd_test.go @@ -3,6 +3,7 @@ package main import ( "fmt" "io/ioutil" + "net/http" "os" "path/filepath" "regexp" @@ -10,6 +11,7 @@ import ( "testing" "github.com/facebookgo/ensure" + "github.com/facebookgo/parse" ) func newNewCmdHarness(t testing.TB) (*Harness, *newCmd) { @@ -18,6 +20,69 @@ func newNewCmdHarness(t testing.TB) (*Harness, *newCmd) { return h, &newCmd{} } +func TestCloudCodeHelpMessage(t *testing.T) { + t.Parallel() + h := newHarness(t) + defer h.Stop() + + n := &newCmd{} + msg := n.cloudCodeHelpMessage(h.env, &app{ApplicationID: "a", MasterKey: "m"}) + ensure.StringContains(t, + msg, + fmt.Sprintf( + `Your Cloud Code has been created at %s. +Next, you might want to deploy this code with "parse deploy". +This includes a "Hello world" cloud function, so once you deploy +you can test that it works, with:`, + h.env.Root, + ), + ) +} + +func TestGetCloudCodeDir(t *testing.T) { + t.Parallel() + h := newHarness(t) + h.makeEmptyRoot() + defer h.Stop() + + n := &newCmd{} + h.env.In = ioutil.NopCloser(strings.NewReader("\n")) + name, err := n.getCloudCodeDir(h.env, "myapp", true) + ensure.Nil(t, err) + ensure.StringContains(t, h.Out.String(), "Now it's time to set up some Cloud Code") + ensure.DeepEqual(t, name, "myapp") + + h.Out.Reset() + h.env.In = ioutil.NopCloser(strings.NewReader("otherApp\n")) + name, err = n.getCloudCodeDir(h.env, "myapp", true) + ensure.Nil(t, err) + ensure.StringContains(t, h.Out.String(), "Now it's time to set up some Cloud Code") + ensure.DeepEqual(t, name, "otherApp") + + _, err = os.Create(filepath.Join(h.env.Root, "otherApp")) + ensure.Nil(t, err) + h.Out.Reset() + h.env.In = ioutil.NopCloser(strings.NewReader("otherApp\n")) + name, err = n.getCloudCodeDir(h.env, "myapp", true) + ensure.Err(t, err, regexp.MustCompile("already exists")) + ensure.StringContains(t, h.Out.String(), "Now it's time to set up some Cloud Code") + + ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, "myapp", "config"), 0755)) + _, err = os.Create(filepath.Join(h.env.Root, "myapp", "config", "global.json")) + ensure.Nil(t, err) + h.Out.Reset() + h.env.In = ioutil.NopCloser(strings.NewReader("\n")) + name, err = n.getCloudCodeDir(h.env, "myapp", true) + ensure.Err(t, err, regexp.MustCompile("you already have Cloud Code")) + ensure.Nil(t, os.Remove(filepath.Join(h.env.Root, "myapp", "config", "global.json"))) + + h.Out.Reset() + h.env.In = ioutil.NopCloser(strings.NewReader("\n")) + name, err = n.getCloudCodeDir(h.env, "myapp", false) + ensure.Nil(t, err) + ensure.StringContains(t, h.Out.String(), "folder where we can download the latest") +} + func TestNewCmdDirs(t *testing.T) { t.Parallel() @@ -104,4 +169,50 @@ func TestShouldCreateNewApp(t *testing.T) { h.env.In = ioutil.NopCloser(strings.NewReader("other")) _, err = n.promptCreateNewApp(h.env, false) ensure.Err(t, err, regexp.MustCompile("are the only valid options")) + + decision, err = n.promptCreateNewApp(h.env, true) + ensure.Nil(t, err) + ensure.DeepEqual(t, decision, "existing") + + n.createNewApp = true + decision, err = n.promptCreateNewApp(h.env, true) + ensure.Nil(t, err) + ensure.DeepEqual(t, decision, "new") +} + +func TestSetupAndConfigure(t *testing.T) { + t.Parallel() + h := newHarness(t) + h.makeEmptyRoot() + defer h.Stop() + + n := &newCmd{} + h.env.Type = parseFormat + h.env.In = ioutil.NopCloser(strings.NewReader("\n")) + code, err := n.setupSample(h.env, "myapp", &parseAppConfig{ApplicationID: "a"}, true, false) + ensure.Nil(t, err) + ensure.True(t, code) + + ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.URL.Path, "/1/jsVersions") + rows := jsSDKVersion{JS: []string{"1.5.0", "1.6.0"}} + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), + }, nil + }) + h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + err = n.configureSample( + &addCmd{MakeDefault: true}, + "yolo", + &parseAppConfig{ApplicationID: "a", masterKey: "m"}, + nil, + h.env, + ) + ensure.Nil(t, err) + + d := &defaultCmd{} + h.Out.Reset() + ensure.Nil(t, d.run(h.env, nil)) + ensure.DeepEqual(t, h.Out.String(), "Current default app is yolo\n") } diff --git a/update_cmd.go b/update_cmd.go index 1957399..7a10ac8 100644 --- a/update_cmd.go +++ b/update_cmd.go @@ -41,16 +41,16 @@ func (u *updateCmd) latestVersion(e *env) (string, error) { return res.Version, nil } -func (u *updateCmd) updateCLI(e *env) (bool, error) { +func (u *updateCmd) getDownloadURL(e *env) (string, error) { ostype := runtime.GOOS arch := runtime.GOARCH latestVersion, err := u.latestVersion(e) if err != nil { - return false, err + return "", err } if latestVersion == "" || latestVersion == version { - return false, nil + return "", nil } var downloadURL string @@ -66,7 +66,17 @@ func (u *updateCmd) updateCLI(e *env) (bool, error) { downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, linuxDownload) } } + return downloadURL, nil +} +func (u *updateCmd) updateCLI(e *env) (bool, error) { + downloadURL, err := u.getDownloadURL(e) + if err != nil { + return false, err + } + if downloadURL == "" { + return false, nil + } exec, err := osext.Executable() if err != nil { return false, stackerr.Wrap(err) diff --git a/update_cmd_test.go b/update_cmd_test.go index bbea48a..e56cb26 100644 --- a/update_cmd_test.go +++ b/update_cmd_test.go @@ -33,4 +33,8 @@ func TestLatestVersion(t *testing.T) { latestVersion, err := u.latestVersion(h.env) ensure.Nil(t, err) ensure.DeepEqual(t, latestVersion, "2.0.2") + + downloadURL, err := u.getDownloadURL(h.env) + ensure.StringContains(t, + downloadURL, "https://github.com/ParsePlatform/parse-cli/releases/download/release_2.0.2") } From 649c5ccbd5e76b29eeca8a08d55d80b13c764d33 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 24 Sep 2015 11:36:26 -0700 Subject: [PATCH 058/106] [tests] add more tests to improve coverage --- apps.go | 2 +- apps_test.go | 14 ++++++++++++ list_cmd_test.go | 14 ++++++++++++ main_test.go | 29 +++++++++++++++++++++++++ migrate_cmd_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ symbols_cmd_test.go | 18 ++++++++++++++++ 6 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 symbols_cmd_test.go diff --git a/apps.go b/apps.go index 379ee0b..5908c5a 100644 --- a/apps.go +++ b/apps.go @@ -248,7 +248,7 @@ func (a *apps) createApp(e *env, givenName string) (*app, error) { } if _, ok := appNames[appName]; ok { - fmt.Fprintf(e.Out, + fmt.Fprintf(e.Err, `Hey! you already created an app named %q. The apps associated with your account: %s diff --git a/apps_test.go b/apps_test.go index 4c6904d..f5d21c5 100644 --- a/apps_test.go +++ b/apps_test.go @@ -259,6 +259,20 @@ RevokeSessionOnPasswordChange true `) } +func TestCreateNewAppNameTaken(t *testing.T) { + t.Parallel() + + h, _ := newAppHarness(t) + defer h.Stop() + + a := defaultApps + + h.env.In = ioutil.NopCloser(strings.NewReader("A\nD")) + _, err := a.createApp(h.env, "") + ensure.Nil(t, err) + ensure.StringContains(t, h.Err.String(), "already created an app") +} + func TestNoPanicAppCreate(t *testing.T) { t.Parallel() h := newHarness(t) diff --git a/list_cmd_test.go b/list_cmd_test.go index b2ed337..7a1b644 100644 --- a/list_cmd_test.go +++ b/list_cmd_test.go @@ -1,6 +1,8 @@ package main import ( + "io/ioutil" + "strings" "testing" "github.com/facebookgo/ensure" @@ -70,3 +72,15 @@ func TestPrintListNoApps(t *testing.T) { n.cloneSampleCloudCode(h.env, true) ensure.Nil(t, l.printListOfApps(h.env)) } + +func TestRunListCmd(t *testing.T) { + t.Parallel() + + h, _ := newAppHarness(t) + defer h.Stop() + + l := &listCmd{} + h.env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) + ensure.Nil(t, l.run(h.env, []string{"A"})) + ensure.StringContains(t, h.Out.String(), `Properties of the app "A"`) +} diff --git a/main_test.go b/main_test.go index 8b9c36f..495e367 100644 --- a/main_test.go +++ b/main_test.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "regexp" + "strings" "testing" "github.com/facebookgo/clock" @@ -375,3 +376,31 @@ func TestMultiErrorString(t *testing.T) { ensure.StringContains(t, errStr, `parse: api error with code=1 and message="message"`) ensure.StringContains(t, errStr, ".go") } + +func TestRunWithContext(t *testing.T) { + t.Parallel() + + h := newHarness(t) + h.makeEmptyRoot() + defer h.Stop() + + n := &newCmd{} + h.env.Type = parseFormat + h.env.In = ioutil.NopCloser(strings.NewReader("\n")) + _, err := n.setupSample(h.env, + "yolo", + &parseAppConfig{ + ApplicationID: "yolo-id", + MasterKey: "yoda", + }, + false, + false, + ) + ensure.Nil(t, err) + + dummy := func(e *env, c *context) error { + ensure.DeepEqual(t, c.AppConfig.getApplicationID(), "yolo-id") + return nil + } + runWithAppClient(h.env, dummy) +} diff --git a/migrate_cmd_test.go b/migrate_cmd_test.go index 26dda52..77fd429 100644 --- a/migrate_cmd_test.go +++ b/migrate_cmd_test.go @@ -1,7 +1,11 @@ package main import ( + "io/ioutil" + "os" + "path/filepath" "regexp" + "strings" "testing" "github.com/facebookgo/ensure" @@ -76,3 +80,51 @@ func TestUpgradeLegacyWithEmail(t *testing.T) { ensure.DeepEqual(t, config.Applications["app"].MasterKey, "") ensure.DeepEqual(t, config.getProjectConfig().ParserEmail, "test@email.com") } + +func TestRunMigrateCmd(t *testing.T) { + t.Parallel() + + h := newHarness(t) + h.makeEmptyRoot() + defer h.Stop() + + n := &newCmd{} + h.env.Type = parseFormat + h.env.In = ioutil.NopCloser(strings.NewReader("\n")) + _, err := n.setupSample(h.env, + "yolo", + &parseAppConfig{ + ApplicationID: "yolo-id", + MasterKey: "yoda", + }, + false, + false, + ) + ensure.Nil(t, err) + + m := &migrateCmd{} + err = m.run(h.env) + ensure.Err(t, err, regexp.MustCompile("Already using the preferred config format")) + + ensure.Nil(t, os.Remove(filepath.Join(h.env.Root, parseLocal))) + ensure.Nil(t, os.Remove(filepath.Join(h.env.Root, parseProject))) + + ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) + ensure.Nil(t, + ioutil.WriteFile( + filepath.Join(h.env.Root, legacyConfigFile), + []byte(`{ + "applications": { + "yolo": { + "applicationId": "yolo-id", + "masterKey": "yoda" + } + } + }`), + 0600, + ), + ) + + ensure.Nil(t, m.run(h.env)) + ensure.StringContains(t, h.Out.String(), "Successfully migrated") +} diff --git a/symbols_cmd_test.go b/symbols_cmd_test.go new file mode 100644 index 0000000..75591a5 --- /dev/null +++ b/symbols_cmd_test.go @@ -0,0 +1,18 @@ +package main + +import ( + "regexp" + "testing" + + "github.com/facebookgo/ensure" +) + +func TestRunSymbolsCmd(t *testing.T) { + t.Parallel() + + h := newHarness(t) + defer h.Stop() + + s := &symbolsCmd{} + ensure.Err(t, s.run(h.env, nil), regexp.MustCompile("Please specify path")) +} From f654bbb9a7c8770a69737a16a5dd8714953068b6 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 24 Sep 2015 13:42:32 -0700 Subject: [PATCH 059/106] [new] clairfy documentation of new cmd the code location flag is relative to the current directory for ease of use documentation does not make this clear. fix it. --- new.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/new.go b/new.go index eabf6e4..52c10bb 100644 --- a/new.go +++ b/new.go @@ -359,6 +359,6 @@ You can also use it in non-interactive mode by using the various flags available cmd.Flags().StringVarP(&nc.parseAppName, "app", "a", nc.parseAppName, "Name of the Parse app you want to create or set up Cloud Code project for.") cmd.Flags().StringVarP(&nc.codeLocation, "loc", "l", nc.codeLocation, - "Location at which the Cloud Code project will be set up.") + "Location, relative to the current directory, at which the Cloud Code project will be set up.") return cmd } From a1eaa197521f9beef49bd0c8f3f0f0f39e8363ac Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 27 Sep 2015 23:59:05 +0000 Subject: [PATCH 060/106] [update] do not check version support at update currently we check and report unsupported versions even during `parse update` this diff checks against that. since operation is not needed --- commands.go | 6 +++--- main.go | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/commands.go b/commands.go index 9137d0e..0ee32ec 100644 --- a/commands.go +++ b/commands.go @@ -8,7 +8,7 @@ import ( "github.com/spf13/cobra" ) -func parseRootCmd(e *env) *cobra.Command { +func parseRootCmd(e *env) ([]string, *cobra.Command) { c := &cobra.Command{ Use: "parse", Long: fmt.Sprintf( @@ -45,7 +45,7 @@ http://parse.com`, c.AddCommand(newVersionCmd(e)) if len(os.Args) <= 1 { - return c + return nil, c } commands := []string{"help"} @@ -61,5 +61,5 @@ http://parse.com`, } c.SetArgs(args) - return c + return args, c } diff --git a/main.go b/main.go index 9d55fe5..3586dd4 100644 --- a/main.go +++ b/main.go @@ -100,19 +100,24 @@ func main() { } e.ParseAPIClient = apiClient - var rootCmd *cobra.Command + var ( + rootCmd *cobra.Command + command []string + ) switch e.Type { case legacyParseFormat, parseFormat: - rootCmd = parseRootCmd(&e) + command, rootCmd = parseRootCmd(&e) } - message, err := checkIfSupported(&e, version, os.Args[1:]...) - if err != nil { - fmt.Fprintln(e.Err, err) - os.Exit(1) - } - if message != "" { - fmt.Fprintln(e.Err, message) + if len(command) == 0 || command[0] != "update" { + message, err := checkIfSupported(&e, version, command...) + if err != nil { + fmt.Fprintln(e.Err, err) + os.Exit(1) + } + if message != "" { + fmt.Fprintln(e.Err, message) + } } if err := rootCmd.Execute(); err != nil { From 15d8ca12ce439cdd4f0b0fb94313d86787544607 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 28 Sep 2015 00:05:39 +0000 Subject: [PATCH 061/106] [accountkey] add descriptive message at creation add a descriptive message of what happens at key creation time --- configure_cmd_test.go | 2 +- login.go | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/configure_cmd_test.go b/configure_cmd_test.go index db0107d..60468c1 100644 --- a/configure_cmd_test.go +++ b/configure_cmd_test.go @@ -26,7 +26,7 @@ func TestConfigureAccountKey(t *testing.T) { t, h.Out.String(), ` -Input your account key or press enter to generate a new one. +Input your account key or press ENTER to generate a new one. `) h.env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) diff --git a/login.go b/login.go index fd4f8ca..bc2cc52 100644 --- a/login.go +++ b/login.go @@ -298,7 +298,14 @@ Please login to Parse using your email and password.`, func (l *login) helpCreateToken(e *env) (string, error) { for i := 0; i < 4; i++ { - fmt.Fprintln(e.Out, "\nInput your account key or press enter to generate a new one.") + fmt.Fprintf(e.Out, ` +Input your account key or press ENTER to generate a new one. +NOTE: on pressing ENTER we'll try to open the url: + %q +in default browser. +`, + keysURL, + ) fmt.Fprintf(e.Out, `Account Key: `) var token string From 5632dce789e914f2e36a2a6f3cd1842640c49834 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 27 Sep 2015 17:23:14 -0700 Subject: [PATCH 062/106] [bump-version] bump version to 2.2.8 --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 3586dd4..362c32f 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( ) const ( - version = "2.2.7" + version = "2.2.8" cloudDir = "cloud" hostingDir = "public" defaultBaseURL = "https://api.parse.com/1/" From a9a5da5f0a281a2784e778d7d50de0c6e6aaac46 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 2 Oct 2015 06:16:01 +0000 Subject: [PATCH 063/106] [new] just create app, no cloud code project allow for users to just create an app, without setting up a cloud code project. this is helpful if one wants to create multiple apps using the cli. ```bash parse -nca yolo # just creates a new parse app, and prints its properties parse -na yolo # just prints the properties of app "yolo", not preferred ``` --- apps.go | 7 +++++-- apps_test.go | 4 ++-- configure_cmd.go | 8 +++++--- new.go | 33 +++++++++++++++++++++++++-------- new_cmd_test.go | 22 ++++++++++++++++++++++ 5 files changed, 59 insertions(+), 15 deletions(-) diff --git a/apps.go b/apps.go index 5908c5a..183ebde 100644 --- a/apps.go +++ b/apps.go @@ -227,7 +227,7 @@ func (a *apps) restCreateApp(e *env, appName string) (*app, error) { return &res, nil } -func (a *apps) createApp(e *env, givenName string) (*app, error) { +func (a *apps) createApp(e *env, givenName string, retries int) (*app, error) { apps, err := a.restFetchApps(e) if err != nil { return nil, err @@ -236,7 +236,10 @@ func (a *apps) createApp(e *env, givenName string) (*app, error) { for _, app := range apps { appNames[app.Name] = struct{}{} } - for i := 0; i < numRetries; i++ { + if retries <= 0 { + retries = numRetries + } + for i := 0; i < retries; i++ { var appName string if givenName != "" { appName = givenName diff --git a/apps_test.go b/apps_test.go index f5d21c5..1181ae9 100644 --- a/apps_test.go +++ b/apps_test.go @@ -234,7 +234,7 @@ func TestCreateNewApp(t *testing.T) { a := defaultApps h.env.In = ioutil.NopCloser(strings.NewReader("D")) - app, err := a.createApp(h.env, "") + app, err := a.createApp(h.env, "", 0) ensure.Nil(t, err) ensure.Nil(t, a.printApp(h.env, app)) @@ -268,7 +268,7 @@ func TestCreateNewAppNameTaken(t *testing.T) { a := defaultApps h.env.In = ioutil.NopCloser(strings.NewReader("A\nD")) - _, err := a.createApp(h.env, "") + _, err := a.createApp(h.env, "", 0) ensure.Nil(t, err) ensure.StringContains(t, h.Err.String(), "already created an app") } diff --git a/configure_cmd.go b/configure_cmd.go index dbb4f53..e73db70 100644 --- a/configure_cmd.go +++ b/configure_cmd.go @@ -644,9 +644,11 @@ func newConfigureCmd(e *env) *cobra.Command { cmd.AddCommand(projectCmd) hooksCmd := &cobra.Command{ - Use: "hooks [filename]", - Short: "Configure webhooks according to given config file", - Long: "Configure webhooks for the app based on the given configuration csv file.", + Use: "hooks [filename]", + Short: "Configure webhooks according to given config file", + Long: `Configure webhooks for the app based on the given configuration csv file. +For more details read: https://parse.com/docs/js/guide#command-line-webhooks +`, Run: runWithArgsClient(e, c.hooksCmd), Aliases: []string{"webhooks"}, } diff --git a/new.go b/new.go index 52c10bb..8942fa2 100644 --- a/new.go +++ b/new.go @@ -14,7 +14,8 @@ import ( type newCmd struct { addApplication bool - noCode bool // do not set up/download code + noCode bool // do not setup a Cloud Code project + configOnly bool // create a Cloud Code project with only configuration, no code createNewApp bool // create a new app parseAppName string // name of parse app codeLocation string // location of cloud code project @@ -150,7 +151,7 @@ func (n *newCmd) createConfigWithContent(path, content string) error { } func (n *newCmd) promptCreateNewApp(e *env, nonInteractive bool) (string, error) { - if nonInteractive { + if nonInteractive || n.noCode { if n.createNewApp { return "new", nil } @@ -205,7 +206,7 @@ Please refrain from creating a Parse project inside another Parse project. if nonInteractive { cloudCodeDir = n.codeLocation - } else if n.noCode { + } else if n.configOnly { cloudCodeDir = "" // ensures that "parse new --init" inits the current directory } else { cloudCodeDir, err = n.getCloudCodeDir(e, name, isNew) @@ -218,7 +219,7 @@ Please refrain from creating a Parse project inside another Parse project. switch e.Type { case parseFormat: dumpTemplate := false - if !isNew && !n.noCode { + if !isNew && !n.configOnly { // if parse app was already created try to fetch cloud code and populate dir masterKey, err := appConfig.getMasterKey(e) if err != nil { @@ -248,7 +249,7 @@ This will download Cloud Code to a temporary location. } } } - dumpTemplate = (isNew || dumpTemplate) && !n.noCode + dumpTemplate = (isNew || dumpTemplate) && !n.configOnly return dumpTemplate, n.cloneSampleCloudCode(e, dumpTemplate) } return false, stackerr.Newf("Unknown project type: %d", e.Type) @@ -304,15 +305,29 @@ func (n *newCmd) run(e *env) error { switch decision { case "new", "n": isNew = true - app, err = apps.createApp(e, n.parseAppName) + var createRetries int + if n.noCode { + createRetries = 1 + } + // we pass retries so that even in non interactive mode we can create an app, + // and failure does not print 3 times to the screen + app, err = apps.createApp(e, n.parseAppName, createRetries) if err != nil { return err } + if n.noCode { + fmt.Fprintln(e.Out, "Successfully created the app.") + return apps.printApp(e, app) + } case "existing", "e": app, err = addCmd.selectApp(e, n.parseAppName) if err != nil { return err } + if n.noCode { + fmt.Fprintln(e.Out, "Successfully selected the app.") + return apps.printApp(e, app) + } } e.Type = parseFormat @@ -352,8 +367,10 @@ You can also use it in non-interactive mode by using the various flags available `, Run: runNoArgs(e, nc.run), } - cmd.Flags().BoolVarP(&nc.noCode, "init", "i", nc.noCode, - "Create a Cloud Code project only with configuration, no code.") + cmd.Flags().BoolVarP(&nc.configOnly, "init", "i", nc.configOnly, + "Create a Cloud Code project only with configuration.") + cmd.Flags().BoolVarP(&nc.noCode, "nocode", "n", nc.noCode, + "Do not set up a Cloud Code project for the app. Typically used in conjunction with 'create' option") cmd.Flags().BoolVarP(&nc.createNewApp, "create", "c", nc.createNewApp, "Set this flag to true if you want to create a new Parse app.") cmd.Flags().StringVarP(&nc.parseAppName, "app", "a", nc.parseAppName, diff --git a/new_cmd_test.go b/new_cmd_test.go index 002b262..d64390f 100644 --- a/new_cmd_test.go +++ b/new_cmd_test.go @@ -149,6 +149,28 @@ func TestCurlCommand(t *testing.T) { `) } +func TestCreateNewAppNoCode(t *testing.T) { + t.Parallel() + h, _ := newAppHarness(t) + defer h.Stop() + + h.env.In = ioutil.NopCloser(strings.NewReader("email\npassword")) + n := &newCmd{noCode: true, createNewApp: true, parseAppName: "yolo"} + ensure.Nil(t, n.run(h.env)) + ensure.StringContains(t, h.Out.String(), "Successfully created") +} + +func TestSelectNewAppNoCode(t *testing.T) { + t.Parallel() + h, _ := newAppHarness(t) + defer h.Stop() + + h.env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) + n := &newCmd{noCode: true, parseAppName: "A"} + ensure.Nil(t, n.run(h.env)) + ensure.StringContains(t, h.Out.String(), "Successfully selected") +} + func TestShouldCreateNewApp(t *testing.T) { t.Parallel() h := newHarness(t) From 82ff806b42a640f02085c327ac000144b7215e6f Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 4 Oct 2015 00:05:24 +0000 Subject: [PATCH 064/106] [nit] remove redundant code remove unused go struct - client --- main.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/main.go b/main.go index 362c32f..822c9bb 100644 --- a/main.go +++ b/main.go @@ -34,12 +34,6 @@ type env struct { ParseAPIClient *ParseAPIClient } -type client struct { - Config config - AppName string - AppConfig appConfig -} - func main() { // some parts of apps.go are unable to handle // interrupts, this logic ensures we exit on system interrupts From c105ed370014842d2ceed5d1378372f485426bbd Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 4 Oct 2015 19:27:12 +0000 Subject: [PATCH 065/106] [new] option to create blank parse project prompt the user if they want to set up a blank project or the full template project --- new.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/new.go b/new.go index 8942fa2..544c1e5 100644 --- a/new.go +++ b/new.go @@ -218,6 +218,23 @@ Please refrain from creating a Parse project inside another Parse project. switch e.Type { case parseFormat: + if !n.configOnly { + var decision string + if isNew { + fmt.Fprint(e.Out, ` +You can either set up a blank Cloud Code project or create a template project. +Please type [y] if you wish to set up a template Cloud Code project or [n] for blank project (y/n): `) + } else { + fmt.Fprint(e.Out, ` +You can either set up a blank Cloud Code project or download the current deployed Cloud Code. +Please type [y] if you wish to download the current Cloud Code or [n] for blank project (y/n): `) + } + fmt.Fscanf(e.In, "%s\n", &decision) + if strings.TrimSpace(decision) == "n" { + n.configOnly = true + } + } + dumpTemplate := false if !isNew && !n.configOnly { // if parse app was already created try to fetch cloud code and populate dir From 2aa8f43a5bc191f3e633472e83eb109573ceed58 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sat, 3 Oct 2015 23:37:18 +0000 Subject: [PATCH 066/106] re-organize package structure - refactor code structure to organize in a better way - separate common functionalities into parsecli - separate parse specific functionalities into parsecmd - other generic, [non-parse backend commands] remain at root --- .travis.yml | 4 +- apps_test.go | 291 ------------- commands.go | 65 --- config_test.go | 46 -- configure_cmd.go => configure.go | 84 ++-- configure_cmd_test.go => configure_test.go | 176 ++++---- default_cmd.go => default.go | 17 +- default_cmd_test.go => default_test.go | 58 +-- functions_cmd.go => functions.go | 25 +- functions_cmd_test.go => functions_test.go | 53 +-- list_cmd.go => list.go | 19 +- list_cmd_test.go => list_test.go | 52 +-- login_test.go | 242 ----------- main.go | 89 ++-- main_test.go | 411 ++---------------- new_cmd.go | 74 ---- parse_commands.go | 67 +++ apps.go => parsecli/apps.go | 59 ++- parsecli/apps_test.go | 199 +++++++++ auto_correct.go => parsecli/auto_correct.go | 4 +- .../auto_correct_test.go | 24 +- client.go => parsecli/client.go | 16 +- config.go => parsecli/config.go | 157 ++++--- parsecli/config_test.go | 72 +++ context.go => parsecli/context.go | 20 +- legacy_config.go => parsecli/legacy_config.go | 14 +- .../legacy_config_test.go | 64 +-- login.go => parsecli/login.go | 88 ++-- parsecli/login_test.go | 211 +++++++++ parsecli/main.go | 226 ++++++++++ parse_config.go => parsecli/parse_config.go | 81 ++-- run_helpers.go => parsecli/runners.go | 42 +- parsecli/runners_test.go | 141 ++++++ parsecli/utils.go | 208 +++++++++ parsecli/utils_test.go | 141 ++++++ .../Resources}/AndroidManifest.xml | 0 .../Resources}/Test.xcarchive/Info.plist | 0 .../dSYMs/Test.app.dSYM/Contents/Info.plist | 0 .../Contents/Resources/DWARF/Test | Bin .../Resources}/build_tools/.empty | 0 {Resources => parsecmd/Resources}/mapping.txt | 0 add_cmd.go => parsecmd/add.go | 29 +- add.go => parsecmd/add_common.go | 29 +- add_cmd_test.go => parsecmd/add_test.go | 77 ++-- .../android_symbol_uploader.go | 7 +- .../android_symbol_uploader_test.go | 11 +- deploy_cmd.go => parsecmd/deploy.go | 33 +- deploy_cmd_test.go => parsecmd/deploy_test.go | 203 ++++----- develop_cmd.go => parsecmd/develop.go | 21 +- .../develop_test.go | 23 +- download_cmd.go => parsecmd/download.go | 21 +- .../download_test.go | 31 +- generate_cmd.go => parsecmd/generate.go | 11 +- .../generate_test.go | 41 +- .../ios_symbol_uploader.go | 11 +- .../ios_symbol_uploader_test.go | 23 +- jssdk_cmd.go => parsecmd/jssdk.go | 39 +- jssdk_cmd_test.go => parsecmd/jssdk_test.go | 78 ++-- logs_cmd.go => parsecmd/logs.go | 11 +- logs_cmd_test.go => parsecmd/logs_test.go | 23 +- migrate_cmd.go => parsecmd/migrate.go | 31 +- .../migrate_test.go | 67 +-- new.go => parsecmd/new_common.go | 93 ++-- new_cmd_test.go => parsecmd/new_test.go | 126 +++--- .../parse_symbol_uploader.go | 7 +- .../parse_symbol_uploader_test.go | 41 +- parseignore.go => parsecmd/parseignore.go | 7 +- .../parseignore_test.go | 2 +- releases_cmd.go => parsecmd/releases.go | 13 +- .../releases_test.go | 27 +- rollback_cmd.go => parsecmd/rollback.go | 9 +- .../rollback_test.go | 23 +- symbols_cmd.go => parsecmd/symbols.go | 9 +- .../symbols_test.go | 7 +- templates.go => parsecmd/templates.go | 33 +- parsecmd/utils.go | 68 +++ parsecmd/utils_test.go | 68 +++ triggers_cmd.go => triggers.go | 25 +- triggers_cmd_test.go => triggers_test.go | 57 +-- update_cmd.go => update.go | 17 +- update_cmd_test.go => update_test.go | 11 +- utils.go | 172 +------- utils_test.go | 155 ------- version_cmd.go => version.go | 9 +- version_test.go | 19 + 85 files changed, 2716 insertions(+), 2642 deletions(-) delete mode 100644 apps_test.go delete mode 100644 commands.go delete mode 100644 config_test.go rename configure_cmd.go => configure.go (87%) rename configure_cmd_test.go => configure_test.go (74%) rename default_cmd.go => default.go (62%) rename default_cmd_test.go => default_test.go (58%) rename functions_cmd.go => functions.go (83%) rename functions_cmd_test.go => functions_test.go (80%) rename list_cmd.go => list.go (67%) rename list_cmd_test.go => list_test.go (51%) delete mode 100644 login_test.go delete mode 100644 new_cmd.go create mode 100644 parse_commands.go rename apps.go => parsecli/apps.go (83%) create mode 100644 parsecli/apps_test.go rename auto_correct.go => parsecli/auto_correct.go (96%) rename auto_correct_test.go => parsecli/auto_correct_test.go (73%) rename client.go => parsecli/client.go (87%) rename config.go => parsecli/config.go (51%) create mode 100644 parsecli/config_test.go rename context.go => parsecli/context.go (53%) rename legacy_config.go => parsecli/legacy_config.go (78%) rename legacy_config_test.go => parsecli/legacy_config_test.go (59%) rename login.go => parsecli/login.go (77%) create mode 100644 parsecli/login_test.go create mode 100644 parsecli/main.go rename parse_config.go => parsecli/parse_config.go (54%) rename run_helpers.go => parsecli/runners.go (62%) create mode 100644 parsecli/runners_test.go create mode 100644 parsecli/utils.go create mode 100644 parsecli/utils_test.go rename {Resources => parsecmd/Resources}/AndroidManifest.xml (100%) rename {Resources => parsecmd/Resources}/Test.xcarchive/Info.plist (100%) rename {Resources => parsecmd/Resources}/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Info.plist (100%) rename {Resources => parsecmd/Resources}/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Resources/DWARF/Test (100%) rename {Resources => parsecmd/Resources}/build_tools/.empty (100%) rename {Resources => parsecmd/Resources}/mapping.txt (100%) rename add_cmd.go => parsecmd/add.go (55%) rename add.go => parsecmd/add_common.go (69%) rename add_cmd_test.go => parsecmd/add_test.go (53%) rename android_symbol_uploader.go => parsecmd/android_symbol_uploader.go (94%) rename android_symbol_uploader_test.go => parsecmd/android_symbol_uploader_test.go (94%) rename deploy_cmd.go => parsecmd/deploy.go (93%) rename deploy_cmd_test.go => parsecmd/deploy_test.go (71%) rename develop_cmd.go => parsecmd/develop.go (83%) rename develop_cmd_test.go => parsecmd/develop_test.go (72%) rename download_cmd.go => parsecmd/download.go (91%) rename download_cmd_test.go => parsecmd/download_test.go (73%) rename generate_cmd.go => parsecmd/generate.go (92%) rename generate_cmd_test.go => parsecmd/generate_test.go (76%) rename ios_symbol_uploader.go => parsecmd/ios_symbol_uploader.go (95%) rename ios_symbol_uploader_test.go => parsecmd/ios_symbol_uploader_test.go (78%) rename jssdk_cmd.go => parsecmd/jssdk.go (66%) rename jssdk_cmd_test.go => parsecmd/jssdk_test.go (56%) rename logs_cmd.go => parsecmd/logs.go (86%) rename logs_cmd_test.go => parsecmd/logs_test.go (82%) rename migrate_cmd.go => parsecmd/migrate.go (65%) rename migrate_cmd_test.go => parsecmd/migrate_test.go (52%) rename new.go => parsecmd/new_common.go (81%) rename new_cmd_test.go => parsecmd/new_test.go (52%) rename parse_symbol_uploader.go => parsecmd/parse_symbol_uploader.go (93%) rename parse_symbol_uploader_test.go => parsecmd/parse_symbol_uploader_test.go (72%) rename parseignore.go => parsecmd/parseignore.go (92%) rename parseignore_test.go => parsecmd/parseignore_test.go (99%) rename releases_cmd.go => parsecmd/releases.go (90%) rename releases_cmd_test.go => parsecmd/releases_test.go (73%) rename rollback_cmd.go => parsecmd/rollback.go (82%) rename rollback_cmd_test.go => parsecmd/rollback_test.go (67%) rename symbols_cmd.go => parsecmd/symbols.go (87%) rename symbols_cmd_test.go => parsecmd/symbols_test.go (52%) rename templates.go => parsecmd/templates.go (58%) create mode 100644 parsecmd/utils.go create mode 100644 parsecmd/utils_test.go rename triggers_cmd.go => triggers.go (85%) rename triggers_cmd_test.go => triggers_test.go (83%) rename update_cmd.go => update.go (83%) rename update_cmd_test.go => update_test.go (66%) delete mode 100644 utils_test.go rename version_cmd.go => version.go (58%) create mode 100644 version_test.go diff --git a/.travis.yml b/.travis.yml index 68e0e54..2dd9bf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,9 +2,7 @@ language: go go: - 1.4.2 script: - - go test -v ./... -coverprofile=coverage.txt -covermode=atomic -after_success: - - bash <(curl -s https://codecov.io/bash) + - go test -v ./... before_deploy: - go get github.com/mitchellh/gox - gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" -build-toolchain diff --git a/apps_test.go b/apps_test.go deleted file mode 100644 index 1181ae9..0000000 --- a/apps_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io/ioutil" - "net/http" - "regexp" - "strings" - "testing" - - "github.com/facebookgo/ensure" - "github.com/facebookgo/parse" -) - -var ( - defaultCredentials = credentials{email: "email", password: "password"} - defaultTokenCredentials = credentials{email: "email", token: "token"} - defaultApps = apps{login: login{credentials: defaultCredentials}} - defaultAppsWithToken = apps{login: login{credentials: defaultTokenCredentials}} -) - -func newTestApp(suffix string) *app { - return &app{ - Name: suffix, - DashboardURL: fmt.Sprintf("https://api.example.com/dashboard/%s", suffix), - ApplicationID: fmt.Sprintf("applicationID.%s", suffix), - ClientKey: fmt.Sprintf("clientKey.%s", suffix), - JavaScriptKey: fmt.Sprintf("javaScriptKey.%s", suffix), - WindowsKey: fmt.Sprintf("windowsKey.%s", suffix), - WebhookKey: fmt.Sprintf("webhookKey.%s", suffix), - RestKey: fmt.Sprintf("restKey.%s", suffix), - MasterKey: fmt.Sprintf("masterKey.%s", suffix), - ClientPushEnabled: false, - ClientClassCreationEnabled: true, - RequireRevocableSessions: true, - RevokeSessionOnPasswordChange: true, - } -} - -func newAppHarness(t testing.TB) (*Harness, []*app) { - h := newHarness(t) - - apps := []*app{ - newTestApp("A"), - newTestApp("B"), - } - res := map[string][]*app{"results": apps} - ht := transportFunc(func(r *http.Request) (*http.Response, error) { - - email := r.Header.Get("X-Parse-Email") - password := r.Header.Get("X-Parse-Password") - token := r.Header.Get("X-Parse-Account-Key") - if !((email == "email" && password == "password") || (token == "token")) { - return &http.Response{ - StatusCode: http.StatusUnauthorized, - Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect credentials"}`)), - }, nil - } - - switch r.URL.Path { - case "/1/apps": - if r.Method == "GET" { - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), - }, nil - - } - - if r.Method != "POST" || r.Body == nil { - return &http.Response{ - StatusCode: http.StatusNotFound, - }, errors.New("unknown resource") - } - - var params map[string]string - if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { - return &http.Response{ - StatusCode: http.StatusInternalServerError, - }, err - } - details, err := json.Marshal(newTestApp(params["appName"])) - if err != nil { - return &http.Response{ - StatusCode: http.StatusInternalServerError, - }, err - } - - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader(details)), - }, nil - - case "/1/apps/an-app": - ensure.DeepEqual(t, r.Method, "GET") - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, newTestApp("an-app")))), - }, nil - default: - return &http.Response{ - StatusCode: http.StatusNotFound, - }, nil - } - }) - - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - return h, apps -} - -func TestFetchAppKeys(t *testing.T) { - t.Parallel() - - h, _ := newAppHarness(t) - defer h.Stop() - - h.env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) - app, err := fetchAppKeys(h.env, "an-app") - ensure.Nil(t, err) - ensure.DeepEqual(t, app, newTestApp("an-app")) -} - -func TestFetchApps(t *testing.T) { - t.Parallel() - - h, apps := newAppHarness(t) - defer h.Stop() - - a := defaultApps - gotApps, err := a.restFetchApps(h.env) - ensure.Nil(t, err) - ensure.DeepEqual(t, gotApps, apps) - - a.login.credentials.password = "invalid" - _, err = a.restFetchApps(h.env) - ensure.DeepEqual(t, err, errAuth) -} - -func TestFetchAppsToken(t *testing.T) { - t.Parallel() - - h, apps := newAppHarness(t) - defer h.Stop() - - a := defaultAppsWithToken - gotApps, err := a.restFetchApps(h.env) - ensure.Nil(t, err) - ensure.DeepEqual(t, gotApps, apps) - - a.login.credentials.token = "invalid" - _, err = a.restFetchApps(h.env) - ensure.DeepEqual(t, err, errAuth) -} - -func TestSelectAppWithRetries(t *testing.T) { - t.Parallel() - - h, apps := newAppHarness(t) - - defer h.Stop() - - a := defaultApps - h.env.In = ioutil.NopCloser(strings.NewReader("3\n1")) - selectedApp, err := a.selectApp(apps, "test: ", h.env) - ensure.Nil(t, err) - ensure.DeepEqual(t, h.Out.String(), - `1: A -2: B -test: Invalid selection 3: must be between 1 and 2 -1: A -2: B -test: `) - ensure.DeepEqual(t, selectedApp, newTestApp("A")) -} - -func TestSelectApp(t *testing.T) { - t.Parallel() - - // apps = [B, A] & appNames=[A,B] - h, apps := newAppHarness(t) - apps[0], apps[1] = apps[1], apps[0] - - defer h.Stop() - - a := defaultApps - h.env.In = ioutil.NopCloser(strings.NewReader("1")) - selectedApp, err := a.selectApp(apps, "test: ", h.env) - ensure.Nil(t, err) - ensure.DeepEqual(t, h.Out.String(), - `1: A -2: B -test: `) - ensure.DeepEqual(t, selectedApp, newTestApp("A")) -} - -func TestGetAppName(t *testing.T) { - t.Parallel() - - h := newHarness(t) - defer h.Stop() - - a := defaultApps - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - appName, err := a.getAppName(h.env) - ensure.Err(t, err, regexp.MustCompile("App name cannot be empty")) - - h.env.In = ioutil.NopCloser(strings.NewReader("hello\n")) - appName, err = a.getAppName(h.env) - ensure.Nil(t, err) - ensure.DeepEqual(t, appName, "hello") -} - -func TestCreateApp(t *testing.T) { - t.Parallel() - - h, _ := newAppHarness(t) - defer h.Stop() - - a := defaultApps - app, err := a.restCreateApp(h.env, "C") - - ensure.Nil(t, err) - ensure.DeepEqual(t, app, newTestApp("C")) -} - -func TestCreateNewApp(t *testing.T) { - t.Parallel() - - h, _ := newAppHarness(t) - defer h.Stop() - - a := defaultApps - h.env.In = ioutil.NopCloser(strings.NewReader("D")) - app, err := a.createApp(h.env, "", 0) - ensure.Nil(t, err) - - ensure.Nil(t, a.printApp(h.env, app)) - ensure.DeepEqual(t, h.Out.String(), - `Please choose a name for your Parse app. -Note that this name will appear on the Parse website, -but it does not have to be the same as your mobile app's public name. -Name: Properties of the app "D": -Name D -DashboardURL https://api.example.com/dashboard/D -ApplicationID applicationID.D -ClientKey clientKey.D -JavaScriptKey javaScriptKey.D -WindowsKey windowsKey.D -WebhookKey webhookKey.D -RestKey restKey.D -MasterKey masterKey.D -ClientPushEnabled false -ClientClassCreationEnabled true -RequireRevocableSessions true -RevokeSessionOnPasswordChange true -`) -} - -func TestCreateNewAppNameTaken(t *testing.T) { - t.Parallel() - - h, _ := newAppHarness(t) - defer h.Stop() - - a := defaultApps - - h.env.In = ioutil.NopCloser(strings.NewReader("A\nD")) - _, err := a.createApp(h.env, "", 0) - ensure.Nil(t, err) - ensure.StringContains(t, h.Err.String(), "already created an app") -} - -func TestNoPanicAppCreate(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - - ht := transportFunc(func(r *http.Request) (*http.Response, error) { - ensure.DeepEqual(t, r.URL.Path, "/1/apps") - return nil, errors.New("nil response panic") - }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - - var apps apps - app, err := apps.restCreateApp(h.env, "panic!") - ensure.True(t, app == nil) - ensure.Err(t, err, regexp.MustCompile("nil response panic")) -} diff --git a/commands.go b/commands.go deleted file mode 100644 index 0ee32ec..0000000 --- a/commands.go +++ /dev/null @@ -1,65 +0,0 @@ -package main - -import ( - "fmt" - "os" - "time" - - "github.com/spf13/cobra" -) - -func parseRootCmd(e *env) ([]string, *cobra.Command) { - c := &cobra.Command{ - Use: "parse", - Long: fmt.Sprintf( - `Parse Command Line Interface -Version %v -Copyright %d Parse, Inc. -http://parse.com`, - version, - time.Now().Year(), - ), - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, - } - - c.AddCommand(newAddCmd(e)) - c.AddCommand(newConfigureCmd(e)) - c.AddCommand(newDefaultCmd(e)) - c.AddCommand(newDeployCmd(e)) - c.AddCommand(newDevelopCmd(e)) - c.AddCommand(newDownloadCmd(e)) - c.AddCommand(newFunctionHooksCmd(e)) - c.AddCommand(newGenerateCmd(e)) - c.AddCommand(newJsSdkCmd(e)) - c.AddCommand(newListCmd(e)) - c.AddCommand(newLogsCmd(e)) - c.AddCommand(newMigrateCmd(e)) - c.AddCommand(newNewCmd(e)) - c.AddCommand(newReleasesCmd(e)) - c.AddCommand(newRollbackCmd(e)) - c.AddCommand(newSymbolsCmd(e)) - c.AddCommand(newTriggerHooksCmd(e)) - c.AddCommand(newUpdateCmd(e)) - c.AddCommand(newVersionCmd(e)) - - if len(os.Args) <= 1 { - return nil, c - } - - commands := []string{"help"} - for _, command := range c.Commands() { - commands = append(commands, command.Name()) - } - - args := make([]string, len(os.Args)-1) - copy(args, os.Args[1:]) - - if message := makeCorrections(commands, args); message != "" { - fmt.Fprintln(e.Out, message) - } - c.SetArgs(args) - - return args, c -} diff --git a/config_test.go b/config_test.go deleted file mode 100644 index f1f515f..0000000 --- a/config_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package main - -import ( - "io/ioutil" - "os" - "path/filepath" - "testing" - - "github.com/facebookgo/ensure" -) - -func TestGetConfigFile(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - - h.env.Type = legacyParseFormat - ensure.DeepEqual(t, getConfigFile(h.env), filepath.Join(h.env.Root, legacyConfigFile)) - - h.env.Type = parseFormat - ensure.DeepEqual(t, getConfigFile(h.env), filepath.Join(h.env.Root, parseLocal)) -} - -func TestConfigFromDirFormatType(t *testing.T) { - t.Parallel() - h := newHarness(t) - h.makeEmptyRoot() - defer h.Stop() - - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) - - c, err := configFromDir(h.env.Root) - ensure.Nil(t, err) - - ensure.DeepEqual(t, c.getProjectConfig().Type, parseFormat) - - ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) - ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.env.Root, legacyConfigFile), - []byte("{}"), - 0600), - ) - c, err = configFromDir(h.env.Root) - ensure.Nil(t, err) - - ensure.DeepEqual(t, c.getProjectConfig().Type, legacyParseFormat) -} diff --git a/configure_cmd.go b/configure.go similarity index 87% rename from configure_cmd.go rename to configure.go index e73db70..4b474f5 100644 --- a/configure_cmd.go +++ b/configure.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -43,7 +44,7 @@ delete,className:triggerName ) type configureCmd struct { - login login + login parsecli.Login isDefault bool tokenReader io.Reader // for testing @@ -53,13 +54,13 @@ type configureCmd struct { baseWebhookURL *url.URL } -func (c *configureCmd) accountKey(e *env) error { - token, err := c.login.helpCreateToken(e) +func (c *configureCmd) accountKey(e *parsecli.Env) error { + token, err := c.login.HelpCreateToken(e) if err != nil { return err } - email, err := c.login.authToken(e, token) + email, err := c.login.AuthToken(e, token) if err != nil { fmt.Fprintln(e.Err, "Could not store credentials. Please try again.\n") return err @@ -69,11 +70,11 @@ func (c *configureCmd) accountKey(e *env) error { email = "" } - var l login + var l parsecli.Login if c.tokenReader != nil { - l.tokenReader = c.tokenReader + l.TokenReader = c.tokenReader } - foundEmail, creds, err := l.getTokenCredentials(e, email) + foundEmail, creds, err := l.GetTokenCredentials(e, email) firstEverConfigure := false if stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) && !c.isDefault { firstEverConfigure = true @@ -92,13 +93,13 @@ func (c *configureCmd) accountKey(e *env) error { %q for email: %q `, - last4(token), + parsecli.Last4(token), email, ) } } - err = c.login.storeCredentials(e, email, &credentials{token: token}) + err = c.login.StoreCredentials(e, email, &parsecli.Credentials{Token: token}) if err == nil { if c.isDefault { fmt.Fprintln(e.Out, "Successfully stored default account key.") @@ -117,14 +118,14 @@ for email: %q ` Looks like this is the first time you have configured an account key. Note that "parse new" and "parse list" can automatically pick up a default key if present. -Otherwise, you'll have to explicitly set the PARSER_EMAIL environment variable +Otherwise, you'll have to explicitly set the PARSER_EMAIL common.Environment variable for them to pick the correct account key. Further, if the command line tool cannot find an account key for a configured email it will try to use the default account key. Hence, we are automatically configuring the default account key to be the same as current account key. `, ) - err = c.login.storeCredentials(e, "", &credentials{token: token}) + err = c.login.StoreCredentials(e, "", &parsecli.Credentials{Token: token}) if err != nil { fmt.Fprintln(e.Err, "Could not save account key.") return stackerr.Wrap(err) @@ -139,34 +140,23 @@ To change the default account key in future use: return nil } -func (c *configureCmd) parserEmail(e *env, args []string) error { - config, err := configFromDir(e.Root) - if err != nil { - return err - } +func (c *configureCmd) parserEmail(e *parsecli.Env, args []string) error { if len(args) != 1 { return fmt.Errorf("Invalid args: %v, only an email argument is expected.", args) } - config.getProjectConfig().ParserEmail = args[0] - err = storeProjectConfig(e, config) - if err != nil { - fmt.Fprintln(e.Err, "Could not set parser email for project.") - return err - } - fmt.Fprintf(e.Out, "Successfully configured email for current project to: %q\n", args[0]) - return nil + return parsecli.SetParserEmail(e, args[0]) } -func (c *configureCmd) projectType(e *env, args []string) error { - config, err := configFromDir(e.Root) +func (c *configureCmd) projectType(e *parsecli.Env, args []string) error { + config, err := parsecli.ConfigFromDir(e.Root) if err != nil { return err } if len(args) > 1 { return stackerr.Newf("Invalid args: %v, only an optional project type argument is expected.", args) } - validTypes := map[string]int{"parse": parseFormat} - invertedTypes := map[int]string{parseFormat: "parse"} + validTypes := map[string]int{"parse": parsecli.ParseFormat} + invertedTypes := map[int]string{parsecli.ParseFormat: "parse"} numKeys := len(validTypes) var validKeys []string for key := range validTypes { @@ -212,8 +202,8 @@ Enter a number between 1 and %d: `, return stackerr.Newf("Could not make a selection. Please try again.") } - config.getProjectConfig().Type = selectedProjectType - if err := storeProjectConfig(e, config); err != nil { + config.GetProjectConfig().Type = selectedProjectType + if err := parsecli.StoreProjectConfig(e, config); err != nil { fmt.Fprintln(e.Err, "Could not save selected project type to project config") return err } @@ -245,7 +235,7 @@ func (c *configureCmd) checkTriggerName(s string) error { } func (c *configureCmd) postOrPutHook( - e *env, + e *parsecli.Env, hooksOps []*hookOperation, fields ...string, ) (bool, []*hookOperation, error) { @@ -277,7 +267,7 @@ func (c *configureCmd) postOrPutHook( } func (c *configureCmd) deleteHook( - e *env, + e *parsecli.Env, hooksOps []*hookOperation, fields ...string, ) (bool, []*hookOperation, error) { @@ -308,7 +298,7 @@ func (c *configureCmd) deleteHook( } func (c *configureCmd) appendHookOperation( - e *env, + e *parsecli.Env, fields []string, hooks []*hookOperation, ) (bool, []*hookOperation, error) { @@ -326,7 +316,7 @@ func (c *configureCmd) appendHookOperation( return false, nil, stackerr.Wrap(errInvalidFormat) } -func (c *configureCmd) processHooksOperation(e *env, op string) ([]string, error) { +func (c *configureCmd) processHooksOperation(e *parsecli.Env, op string) ([]string, error) { op = strings.TrimSpace(op) if op == "" { return nil, nil @@ -374,7 +364,7 @@ func (c *configureCmd) processHooksOperation(e *env, op string) ([]string, error } func (c *configureCmd) createHooksOperations( - e *env, + e *parsecli.Env, reader io.Reader, ) ([]*hookOperation, error) { scanner := bufio.NewScanner(reader) @@ -427,7 +417,7 @@ func (c *configureCmd) checkStrictMode(restOp string, exists bool) (string, bool return restOp, false, nil } -func (c *configureCmd) functionHookExists(e *env, name string) (bool, error) { +func (c *configureCmd) functionHookExists(e *parsecli.Env, name string) (bool, error) { functionsURL, err := url.Parse(path.Join(defaultFunctionsURL, name)) if err != nil { return false, stackerr.Wrap(err) @@ -450,7 +440,7 @@ func (c *configureCmd) functionHookExists(e *env, name string) (bool, error) { return false, nil } -func (c *configureCmd) deployFunctionHook(e *env, op *hookOperation) error { +func (c *configureCmd) deployFunctionHook(e *parsecli.Env, op *hookOperation) error { if op.function == nil { return stackerr.New("cannot deploy nil function hook") } @@ -479,7 +469,7 @@ func (c *configureCmd) deployFunctionHook(e *env, op *hookOperation) error { return stackerr.Wrap(errInvalidFormat) } -func (c *configureCmd) triggerHookExists(e *env, className, triggerName string) (bool, error) { +func (c *configureCmd) triggerHookExists(e *parsecli.Env, className, triggerName string) (bool, error) { triggersURL, err := url.Parse(path.Join(defaultTriggersURL, className, triggerName)) if err != nil { return false, stackerr.Wrap(err) @@ -502,7 +492,7 @@ func (c *configureCmd) triggerHookExists(e *env, className, triggerName string) return false, nil } -func (c *configureCmd) deployTriggerHook(e *env, op *hookOperation) error { +func (c *configureCmd) deployTriggerHook(e *parsecli.Env, op *hookOperation) error { if op.trigger == nil { return stackerr.New("cannot deploy nil trigger hook") } @@ -532,7 +522,7 @@ func (c *configureCmd) deployTriggerHook(e *env, op *hookOperation) error { } -func (c *configureCmd) deployWebhooksConfig(e *env, hooksOps []*hookOperation) error { +func (c *configureCmd) deployWebhooksConfig(e *parsecli.Env, hooksOps []*hookOperation) error { for _, op := range hooksOps { if op.function == nil && op.trigger == nil { return stackerr.New("hook operation is neither a function, not a trigger.") @@ -554,7 +544,7 @@ func (c *configureCmd) deployWebhooksConfig(e *env, hooksOps []*hookOperation) e return nil } -func (c *configureCmd) parseBaseURL(e *env) error { +func (c *configureCmd) parseBaseURL(e *parsecli.Env) error { if c.baseURL != "" { u, err := url.Parse(c.baseURL) if err != nil { @@ -569,7 +559,7 @@ func (c *configureCmd) parseBaseURL(e *env) error { return nil } -func (c *configureCmd) hooksCmd(e *env, ctx *context, args []string) error { +func (c *configureCmd) hooksCmd(e *parsecli.Env, ctx *parsecli.Context, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid args: %v, only an optional hooks config file is expected.", args) } @@ -603,7 +593,7 @@ func (c *configureCmd) hooksCmd(e *env, ctx *context, args []string) error { return nil } -func newConfigureCmd(e *env) *cobra.Command { +func NewConfigureCmd(e *parsecli.Env) *cobra.Command { var c configureCmd cmd := &cobra.Command{ @@ -619,7 +609,7 @@ func newConfigureCmd(e *env) *cobra.Command { Use: "accountkey", Short: "Store Parse account key on machine", Long: "Stores Parse account key in ~/.parse/netrc.", - Run: runNoArgs(e, c.accountKey), + Run: parsecli.RunNoArgs(e, c.accountKey), Aliases: []string{"key"}, } keyCmd.Flags().BoolVarP(&c.isDefault, "default", "d", c.isDefault, @@ -630,7 +620,7 @@ func newConfigureCmd(e *env) *cobra.Command { Use: "email [id]", Short: "Configures the parser email for this project", Long: "Configures the parser email for current project.", - Run: runWithArgs(e, c.parserEmail), + Run: parsecli.RunWithArgs(e, c.parserEmail), Aliases: []string{"user"}, } cmd.AddCommand(emailCmd) @@ -639,7 +629,7 @@ func newConfigureCmd(e *env) *cobra.Command { Use: "project [type]", Short: "Set the project type to one among listed options", Long: "Set the project type to one among listed options. For instance, 'parse'", - Run: runWithArgs(e, c.projectType), + Run: parsecli.RunWithArgs(e, c.projectType), } cmd.AddCommand(projectCmd) @@ -649,7 +639,7 @@ func newConfigureCmd(e *env) *cobra.Command { Long: `Configure webhooks for the app based on the given configuration csv file. For more details read: https://parse.com/docs/js/guide#command-line-webhooks `, - Run: runWithArgsClient(e, c.hooksCmd), + Run: parsecli.RunWithArgsClient(e, c.hooksCmd), Aliases: []string{"webhooks"}, } hooksCmd.Flags().BoolVarP(&c.hooksStrict, "strict", "s", c.hooksStrict, diff --git a/configure_cmd_test.go b/configure_test.go similarity index 74% rename from configure_cmd_test.go rename to configure_test.go index 60468c1..f05c363 100644 --- a/configure_cmd_test.go +++ b/configure_test.go @@ -9,19 +9,20 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) func TestConfigureAccountKey(t *testing.T) { t.Parallel() - h := newTokenHarness(t) + h := parsecli.NewTokenHarness(t) defer h.Stop() - c := configureCmd{login: login{tokenReader: strings.NewReader("")}} + c := configureCmd{login: parsecli.Login{TokenReader: strings.NewReader("")}} - h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) - ensure.Nil(t, c.accountKey(h.env)) + h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.Env)) ensure.StringContains( t, h.Out.String(), @@ -29,14 +30,14 @@ func TestConfigureAccountKey(t *testing.T) { Input your account key or press ENTER to generate a new one. `) - h.env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) - ensure.Err(t, c.accountKey(h.env), regexp.MustCompile("is not valid")) + h.Env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) + ensure.Err(t, c.accountKey(h.Env), regexp.MustCompile("is not valid")) ensure.DeepEqual(t, h.Err.String(), "Could not store credentials. Please try again.\n\n", ) - h.env.Server = "http://api.parse.com/1/" + h.Env.Server = "http://api.parse.com/1/" c.tokenReader = strings.NewReader( `machine api.parse.com#email login default @@ -44,15 +45,15 @@ Input your account key or press ENTER to generate a new one. `, ) h.Err.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) - ensure.Nil(t, c.accountKey(h.env)) + h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.Env)) ensure.DeepEqual(t, h.Err.String(), `Note: this operation will overwrite the account key: "*oken" for email: "email" `) - h.env.Server = "http://api.parse.com/1/" + h.Env.Server = "http://api.parse.com/1/" c.tokenReader = strings.NewReader( `machine api.parse.com#email login default @@ -61,11 +62,11 @@ for email: "email" ) c.isDefault = true h.Err.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) - ensure.Nil(t, c.accountKey(h.env)) + h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.Env)) ensure.DeepEqual(t, h.Err.String(), "") - h.env.Server = "http://api.parse.com/1/" + h.Env.Server = "http://api.parse.com/1/" c.tokenReader = strings.NewReader( `machine api.parse.com login default @@ -74,11 +75,11 @@ for email: "email" ) c.isDefault = true h.Err.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) - ensure.Nil(t, c.accountKey(h.env)) + h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.Env)) ensure.DeepEqual(t, h.Err.String(), "Note: this operation will overwrite the default account key\n") - h.env.Server = "http://api.parse.com/1/" + h.Env.Server = "http://api.parse.com/1/" c.tokenReader = strings.NewReader( `machine api.parse.com login default @@ -87,29 +88,28 @@ for email: "email" ) h.Err.Reset() c.isDefault = false - h.env.In = ioutil.NopCloser(strings.NewReader("token\n")) - ensure.Nil(t, c.accountKey(h.env)) + h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) + ensure.Nil(t, c.accountKey(h.Env)) ensure.DeepEqual(t, h.Err.String(), "") } func TestParserEmail(t *testing.T) { t.Parallel() - h := newTokenHarness(t) - h.makeEmptyRoot() + h := parsecli.NewTokenHarness(t) + h.MakeEmptyRoot() defer h.Stop() - var n newCmd - ensure.Nil(t, n.createConfigWithContent(filepath.Join(h.env.Root, parseLocal), "{}")) + ensure.Nil(t, parsecli.CreateConfigWithContent(filepath.Join(h.Env.Root, parsecli.ParseLocal), "{}")) ensure.Nil(t, - n.createConfigWithContent( - filepath.Join(h.env.Root, parseProject), + parsecli.CreateConfigWithContent( + filepath.Join(h.Env.Root, parsecli.ParseProject), `{"project_type": 1}`, ), ) var c configureCmd - ensure.Nil(t, c.parserEmail(h.env, []string{"email2"})) + ensure.Nil(t, c.parserEmail(h.Env, []string{"email2"})) ensure.DeepEqual( t, h.Out.String(), @@ -117,38 +117,38 @@ func TestParserEmail(t *testing.T) { `, ) - ensure.Err(t, c.parserEmail(h.env, nil), regexp.MustCompile("Invalid args:")) - ensure.Err(t, c.parserEmail(h.env, []string{"a", "b"}), regexp.MustCompile("Invalid args:")) + ensure.Err(t, c.parserEmail(h.Env, nil), regexp.MustCompile("Invalid args:")) + ensure.Err(t, c.parserEmail(h.Env, []string{"a", "b"}), regexp.MustCompile("Invalid args:")) } func TestProjectType(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.makeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, false)) + h.MakeEmptyRoot() + ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, false)) c := &configureCmd{} - err := c.projectType(h.env, []string{"1", "2"}) + err := c.projectType(h.Env, []string{"1", "2"}) ensure.Err(t, err, regexp.MustCompile("only an optional project type argument is expected")) - h.env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) - err = c.projectType(h.env, nil) + h.Env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) + err = c.projectType(h.Env, nil) ensure.StringContains(t, h.Err.String(), "Invalid selection. Please enter a number") ensure.Err(t, err, regexp.MustCompile("Could not make a selection. Please try again.")) h.Err.Reset() h.Out.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("0\n")) - err = c.projectType(h.env, nil) + h.Env.In = ioutil.NopCloser(strings.NewReader("0\n")) + err = c.projectType(h.Env, nil) ensure.StringContains(t, h.Err.String(), "Please enter a number between 1 and") ensure.Err(t, err, regexp.MustCompile("Could not make a selection. Please try again.")) h.Err.Reset() h.Out.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) - err = c.projectType(h.env, nil) + h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) + err = c.projectType(h.Env, nil) ensure.StringContains(t, h.Out.String(), "Successfully set project type to: parse") ensure.Nil(t, err) } @@ -173,22 +173,22 @@ func TestCheckTriggerName(t *testing.T) { func TestPostOrPutHook(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() c := &configureCmd{} - _, _, err := c.postOrPutHook(h.env, nil, "other") + _, _, err := c.postOrPutHook(h.Env, nil, "other") ensure.Err(t, err, regexp.MustCompile("invalid format")) - _, _, err = c.postOrPutHook(h.env, nil, "post", "1") + _, _, err = c.postOrPutHook(h.Env, nil, "post", "1") ensure.Err(t, err, regexp.MustCompile("invalid format")) - _, _, err = c.postOrPutHook(h.env, nil, "put", "1", "2", "3", "4") + _, _, err = c.postOrPutHook(h.Env, nil, "put", "1", "2", "3", "4") ensure.Err(t, err, regexp.MustCompile("invalid format")) var hooksOps []*hookOperation - _, ops, err := c.postOrPutHook(h.env, hooksOps, + _, ops, err := c.postOrPutHook(h.Env, hooksOps, "post", "call", "https://api.twilio.com/call") ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") @@ -201,7 +201,7 @@ func TestPostOrPutHook(t *testing.T) { }, ) - _, ops, err = c.postOrPutHook(h.env, hooksOps, + _, ops, err = c.postOrPutHook(h.Env, hooksOps, "put", "call", "https://api.twilio.com/call_1") ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") @@ -214,7 +214,7 @@ func TestPostOrPutHook(t *testing.T) { }, ) - _, ops, err = c.postOrPutHook(h.env, hooksOps, "post", + _, ops, err = c.postOrPutHook(h.Env, hooksOps, "post", "_User", "beforeSave", "https://api.twilio.com/message") ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") @@ -228,7 +228,7 @@ func TestPostOrPutHook(t *testing.T) { }, ) - _, ops, err = c.postOrPutHook(h.env, hooksOps, "put", + _, ops, err = c.postOrPutHook(h.Env, hooksOps, "put", "_User", "beforeSave", "https://api.twilio.com/message_1") ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") @@ -242,28 +242,28 @@ func TestPostOrPutHook(t *testing.T) { }, ) - _, ops, err = c.postOrPutHook(h.env, hooksOps, "post", + _, ops, err = c.postOrPutHook(h.Env, hooksOps, "post", "_User", "invalid", "https://other") ensure.Err(t, err, regexp.MustCompile("invalid trigger")) } func TestDeleteHook(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() c := &configureCmd{} - _, _, err := c.deleteHook(h.env, nil, "delete") + _, _, err := c.deleteHook(h.Env, nil, "delete") ensure.Err(t, err, regexp.MustCompile("invalid format")) - _, _, err = c.deleteHook(h.env, nil, "invalid", "1") + _, _, err = c.deleteHook(h.Env, nil, "invalid", "1") ensure.Err(t, err, regexp.MustCompile("invalid format")) - _, _, err = c.deleteHook(h.env, nil, "delete", "1", "2") + _, _, err = c.deleteHook(h.Env, nil, "delete", "1", "2") ensure.Err(t, err, regexp.MustCompile("invalid trigger name")) var hooksOps []*hookOperation - _, ops, err := c.deleteHook(h.env, hooksOps, "delete", "call") + _, ops, err := c.deleteHook(h.Env, hooksOps, "delete", "call") ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") ensure.DeepEqual( @@ -272,7 +272,7 @@ func TestDeleteHook(t *testing.T) { functionHook{FunctionName: "call"}, ) - _, ops, err = c.deleteHook(h.env, hooksOps, "delete", "_User", "beforeSave") + _, ops, err = c.deleteHook(h.Env, hooksOps, "delete", "_User", "beforeSave") ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") ensure.DeepEqual( @@ -285,42 +285,42 @@ func TestDeleteHook(t *testing.T) { func TestProcessHookOperation(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() c := &configureCmd{} - ops, err := c.processHooksOperation(h.env, "\n\t ") + ops, err := c.processHooksOperation(h.Env, "\n\t ") ensure.Nil(t, err) ensure.DeepEqual(t, len(ops), 0) - _, err = c.processHooksOperation(h.env, "invalid") + _, err = c.processHooksOperation(h.Env, "invalid") ensure.Err(t, err, regexp.MustCompile("invalid format")) - _, err = c.processHooksOperation(h.env, "delete,call,caller") + _, err = c.processHooksOperation(h.Env, "delete,call,caller") ensure.Err(t, err, regexp.MustCompile("invalid format")) - ops, err = c.processHooksOperation(h.env, "delete,call") + ops, err = c.processHooksOperation(h.Env, "delete,call") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"delete", "call"}) - ops, err = c.processHooksOperation(h.env, "delete,_User:beforeSave") + ops, err = c.processHooksOperation(h.Env, "delete,_User:beforeSave") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"delete", "_User", "beforeSave"}) - ops, err = c.processHooksOperation(h.env, "post,call,https://twilio.com/call") + ops, err = c.processHooksOperation(h.Env, "post,call,https://twilio.com/call") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) - ops, err = c.processHooksOperation(h.env, "put,call,https://twilio.com/call_1,call_2") + ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2"}) - ops, err = c.processHooksOperation(h.env, "put,call,https://twilio.com/call_1,call_2,call_3") + ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2,call_3") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) - ops, err = c.processHooksOperation(h.env, + ops, err = c.processHooksOperation(h.Env, "pUt,_User:afterDelete,https://twilio.com/message_1,message_2") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"pUt", "_User", "afterDelete", @@ -330,19 +330,19 @@ func TestProcessHookOperation(t *testing.T) { ensure.Nil(t, err) c.baseWebhookURL = u - ops, err = c.processHooksOperation(h.env, "post,call,https://parse.com/call") + ops, err = c.processHooksOperation(h.Env, "post,call,https://parse.com/call") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"post", "call", "https://parse.com/call"}) - ops, err = c.processHooksOperation(h.env, "post,call,https://twilio.com/call") + ops, err = c.processHooksOperation(h.Env, "post,call,https://twilio.com/call") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) - ops, err = c.processHooksOperation(h.env, "put,call,/call_1,call_2,call_3") + ops, err = c.processHooksOperation(h.Env, "put,call,/call_1,call_2,call_3") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"put", "call", "https://parse.com/call_1,call_2,call_3"}) - ops, err = c.processHooksOperation(h.env, "put,call,https://twilio.com/call_1,call_2,call_3") + ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2,call_3") ensure.Nil(t, err) ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) } @@ -350,20 +350,20 @@ func TestProcessHookOperation(t *testing.T) { func TestAppendHookOperation(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() c := &configureCmd{} var hooksOps []*hookOperation - _, ops, err := c.appendHookOperation(h.env, nil, hooksOps) + _, ops, err := c.appendHookOperation(h.Env, nil, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, hooksOps, ops) - _, _, err = c.appendHookOperation(h.env, []string{"invalid"}, nil) + _, _, err = c.appendHookOperation(h.Env, []string{"invalid"}, nil) ensure.Err(t, err, regexp.MustCompile("invalid format")) - _, ops, err = c.appendHookOperation(h.env, + _, ops, err = c.appendHookOperation(h.Env, []string{"post", "call", "https://twilio.com/call"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") @@ -373,7 +373,7 @@ func TestAppendHookOperation(t *testing.T) { functionHook{FunctionName: "call", URL: "https://twilio.com/call"}, ) - _, ops, err = c.appendHookOperation(h.env, + _, ops, err = c.appendHookOperation(h.Env, []string{"put", "call", "https://twilio.com/call_1"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") @@ -384,7 +384,7 @@ func TestAppendHookOperation(t *testing.T) { ) //random stuff - _, ops, err = c.appendHookOperation(h.env, + _, ops, err = c.appendHookOperation(h.Env, []string{"posT", "_User", "afterDelete", "https://twilio.com/message"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") @@ -394,7 +394,7 @@ func TestAppendHookOperation(t *testing.T) { triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message"}, ) - _, ops, err = c.appendHookOperation(h.env, + _, ops, err = c.appendHookOperation(h.Env, []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") @@ -405,7 +405,7 @@ func TestAppendHookOperation(t *testing.T) { ) // random stuff - _, ops, err = c.appendHookOperation(h.env, + _, ops, err = c.appendHookOperation(h.Env, []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1,message_2"}, hooksOps) ensure.Nil(t, err) ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") @@ -459,11 +459,11 @@ func TestCheckStrictMode(t *testing.T) { func TestCreateHookOperations(t *testing.T) { t.Parallel() - h := newHarness(t) - h.makeEmptyRoot() + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() defer h.Stop() - hooksConfigFile := filepath.Join(h.env.Root, "webhooks.csv") + hooksConfigFile := filepath.Join(h.Env.Root, "webhooks.csv") err := ioutil.WriteFile( hooksConfigFile, []byte{}, @@ -475,7 +475,7 @@ func TestCreateHookOperations(t *testing.T) { f, err := os.Open(hooksConfigFile) ensure.Nil(t, err) - ops, err := c.createHooksOperations(h.env, f) + ops, err := c.createHooksOperations(h.Env, f) ensure.Nil(t, err) ensure.DeepEqual(t, len(ops), 0) ensure.Nil(t, f.Close()) @@ -488,7 +488,7 @@ func TestCreateHookOperations(t *testing.T) { f, err = os.Open(hooksConfigFile) ensure.Nil(t, err) - ops, err = c.createHooksOperations(h.env, f) + ops, err = c.createHooksOperations(h.Env, f) ensure.Nil(t, err) ensure.DeepEqual(t, len(ops), 0) ensure.DeepEqual(t, h.Out.String(), "Ignoring line: 1\n") @@ -500,11 +500,11 @@ func TestFunctionHookExists(t *testing.T) { t.Parallel() h := newFunctionsHarness(t) c := &configureCmd{} - exists, err := c.functionHookExists(h.env, "foo") + exists, err := c.functionHookExists(h.Env, "foo") ensure.Nil(t, err) ensure.True(t, exists) - exists, err = c.functionHookExists(h.env, "bar") + exists, err = c.functionHookExists(h.Env, "bar") ensure.Nil(t, err) ensure.False(t, exists) } @@ -515,7 +515,7 @@ func TestDeployFunctionHooks(t *testing.T) { c := &configureCmd{} err := c.deployFunctionHook( - h.env, + h.Env, &hookOperation{ method: "post", function: &functionHook{ @@ -532,11 +532,11 @@ func TestTriggerHookExists(t *testing.T) { t.Parallel() h := newTriggersHarness(t) c := &configureCmd{} - exists, err := c.triggerHookExists(h.env, "foo", "beforeSave") + exists, err := c.triggerHookExists(h.Env, "foo", "beforeSave") ensure.Nil(t, err) ensure.True(t, exists) - exists, err = c.triggerHookExists(h.env, "bar", "other") + exists, err = c.triggerHookExists(h.Env, "bar", "other") ensure.Nil(t, err) ensure.False(t, exists) } @@ -547,7 +547,7 @@ func TestDeployTriggerHooks(t *testing.T) { c := &configureCmd{} err := c.deployTriggerHook( - h.env, + h.Env, &hookOperation{ method: "post", trigger: &triggerHook{ @@ -564,14 +564,14 @@ func TestDeployTriggerHooks(t *testing.T) { func TestParseBaseURL(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() c := &configureCmd{} c.baseURL = "http://hello" - ensure.Err(t, c.parseBaseURL(h.env), regexp.MustCompile("valid https url")) + ensure.Err(t, c.parseBaseURL(h.Env), regexp.MustCompile("valid https url")) c.baseURL = "https://hello" - ensure.Nil(t, c.parseBaseURL(h.env)) + ensure.Nil(t, c.parseBaseURL(h.Env)) ensure.DeepEqual(t, c.baseWebhookURL.String(), c.baseURL) } diff --git a/default_cmd.go b/default.go similarity index 62% rename from default_cmd.go rename to default.go index 1cba6b2..99da61a 100644 --- a/default_cmd.go +++ b/default.go @@ -1,13 +1,14 @@ package main import ( + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) type defaultCmd struct{} -func (d *defaultCmd) run(e *env, args []string) error { +func (d *defaultCmd) run(e *parsecli.Env, args []string) error { var newDefault string if len(args) > 1 { return stackerr.Newf("unexpected arguments, only an optional app name is expected: %v", args) @@ -16,32 +17,32 @@ func (d *defaultCmd) run(e *env, args []string) error { newDefault = args[0] } - config, err := configFromDir(e.Root) + config, err := parsecli.ConfigFromDir(e.Root) if err != nil { return err } - if config.getNumApps() == 0 { + if config.GetNumApps() == 0 { return stackerr.New("No apps are associated with this project. You can add some with parse add") } - defaultApp := config.getDefaultApp() + defaultApp := config.GetDefaultApp() switch newDefault { case "": - return printDefault(e, defaultApp) + return parsecli.PrintDefault(e, defaultApp) default: - return setDefault(e, newDefault, defaultApp, config) + return parsecli.SetDefault(e, newDefault, defaultApp, config) } } -func newDefaultCmd(e *env) *cobra.Command { +func NewDefaultCmd(e *parsecli.Env) *cobra.Command { d := defaultCmd{} return &cobra.Command{ Use: "default [app]", Short: "Sets or gets the default Parse App", Long: `Gets the default Parse App. If an argument is given, sets the ` + `default Parse App.`, - Run: runWithArgs(e, d.run), + Run: parsecli.RunWithArgs(e, d.run), } } diff --git a/default_cmd_test.go b/default_test.go similarity index 58% rename from default_cmd_test.go rename to default_test.go index 4c25232..e82ad14 100644 --- a/default_cmd_test.go +++ b/default_test.go @@ -7,27 +7,35 @@ import ( "regexp" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) -func newDefaultParseConfig(t testing.TB, h *Harness) *parseConfig { - c, err := configFromDir(h.env.Root) +func newDefaultParseConfig(t testing.TB, h *parsecli.Harness) *parsecli.ParseConfig { + c, err := parsecli.ConfigFromDir(h.Env.Root) ensure.Nil(t, err) - config, ok := c.(*parseConfig) + config, ok := c.(*parsecli.ParseConfig) ensure.True(t, ok) - config.Applications = map[string]*parseAppConfig{"first": {}, "second": {}, defaultKey: {Link: "first"}} - config.projectConfig = &projectConfig{Type: legacyParseFormat, Parse: &parseProjectConfig{}} - ensure.Nil(t, storeConfig(h.env, config)) + config.Applications = map[string]*parsecli.ParseAppConfig{ + "first": {}, + "second": {}, + parsecli.DefaultKey: {Link: "first"}, + } + config.ProjectConfig = &parsecli.ProjectConfig{ + Type: parsecli.LegacyParseFormat, + Parse: &parsecli.ParseProjectConfig{}, + } + ensure.Nil(t, parsecli.StoreConfig(h.Env, config)) return config } -func newDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConfig) { - h := newHarness(t) - h.makeEmptyRoot() +func newDefaultCmdHarness(t testing.TB) (*parsecli.Harness, *defaultCmd, *parsecli.ParseConfig) { + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) + ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) config := newDefaultParseConfig(t, h) h.Out.Reset() @@ -40,14 +48,14 @@ func TestPrintDefaultNotSet(t *testing.T) { t.Parallel() h, _, _ := newDefaultCmdHarness(t) defer h.Stop() - ensure.Err(t, printDefault(h.env, ""), regexp.MustCompile("No app is set as default app")) + ensure.Err(t, parsecli.PrintDefault(h.Env, ""), regexp.MustCompile("No app is set as default app")) } func TestPrintDefaultSet(t *testing.T) { t.Parallel() h, _, _ := newDefaultCmdHarness(t) defer h.Stop() - printDefault(h.env, "first") + parsecli.PrintDefault(h.Env, "first") ensure.DeepEqual(t, h.Out.String(), `Current default app is first `) @@ -59,7 +67,7 @@ func TestSetDefaultInvalid(t *testing.T) { defer h.Stop() ensure.Err( t, - setDefault(h.env, "invalid", "", config), + parsecli.SetDefault(h.Env, "invalid", "", config), regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`), ) ensure.DeepEqual(t, h.Out.String(), @@ -73,7 +81,7 @@ func TestSetDefault(t *testing.T) { t.Parallel() h, _, config := newDefaultCmdHarness(t) defer h.Stop() - ensure.Nil(t, setDefault(h.env, "first", "", config)) + ensure.Nil(t, parsecli.SetDefault(h.Env, "first", "", config)) ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") } @@ -81,7 +89,7 @@ func TestSetDefaultWithApp(t *testing.T) { t.Parallel() h, _, config := newDefaultCmdHarness(t) defer h.Stop() - ensure.Nil(t, setDefault(h.env, "first", "first", config)) + ensure.Nil(t, parsecli.SetDefault(h.Env, "first", "first", config)) ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") } @@ -89,17 +97,17 @@ func TestDefaultRunMoreArgs(t *testing.T) { t.Parallel() h, d, _ := newDefaultCmdHarness(t) defer h.Stop() - ensure.Err(t, d.run(h.env, []string{"one", "two"}), regexp.MustCompile(`unexpected arguments, only an optional app name is expected:`)) + ensure.Err(t, d.run(h.Env, []string{"one", "two"}), regexp.MustCompile(`unexpected arguments, only an optional app name is expected:`)) } // NOTE: testing for legacy format -func newLegacyDefaultCmdHarness(t testing.TB) (*Harness, *defaultCmd, *parseConfig) { - h := newHarness(t) - h.makeEmptyRoot() +func newLegacyDefaultCmdHarness(t testing.TB) (*parsecli.Harness, *defaultCmd, *parsecli.ParseConfig) { + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) - ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) - ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.env.Root, legacyConfigFile), + ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) + ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) + ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), []byte("{}"), 0600), ) @@ -117,7 +125,7 @@ func TestLegacySetDefaultInvalid(t *testing.T) { defer h.Stop() ensure.Err( t, - setDefault(h.env, "invalid", "", config), + parsecli.SetDefault(h.Env, "invalid", "", config), regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`), ) ensure.DeepEqual(t, h.Out.String(), @@ -131,7 +139,7 @@ func TestLegacySetDefault(t *testing.T) { t.Parallel() h, _, config := newLegacyDefaultCmdHarness(t) defer h.Stop() - ensure.Nil(t, setDefault(h.env, "first", "", config)) + ensure.Nil(t, parsecli.SetDefault(h.Env, "first", "", config)) ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") } @@ -139,6 +147,6 @@ func TestLegacySetDefaultWithApp(t *testing.T) { t.Parallel() h, _, config := newLegacyDefaultCmdHarness(t) defer h.Stop() - ensure.Nil(t, setDefault(h.env, "first", "first", config)) + ensure.Nil(t, parsecli.SetDefault(h.Env, "first", "first", config)) ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") } diff --git a/functions_cmd.go b/functions.go similarity index 83% rename from functions_cmd.go rename to functions.go index 9cb5a45..5e9eb70 100644 --- a/functions_cmd.go +++ b/functions.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -31,7 +32,7 @@ type functionHooksCmd struct { interactive bool } -func readFunctionName(e *env, params *functionHook) (*functionHook, error) { +func readFunctionName(e *parsecli.Env, params *functionHook) (*functionHook, error) { if params != nil && params.FunctionName != "" { return params, nil } @@ -45,7 +46,7 @@ func readFunctionName(e *env, params *functionHook) (*functionHook, error) { return &f, nil } -func readFunctionParams(e *env, params *functionHook) (*functionHook, error) { +func readFunctionParams(e *parsecli.Env, params *functionHook) (*functionHook, error) { if params != nil && params.FunctionName != "" && params.URL != "" { return params, nil } @@ -67,7 +68,7 @@ func readFunctionParams(e *env, params *functionHook) (*functionHook, error) { const defaultFunctionsURL = "/1/hooks/functions" -func (h *functionHooksCmd) functionHooksCreate(e *env, ctx *context) error { +func (h *functionHooksCmd) functionHooksCreate(e *parsecli.Env, ctx *parsecli.Context) error { params, err := readFunctionParams(e, h.Function) if err != nil { return err @@ -94,7 +95,7 @@ func (h *functionHooksCmd) functionHooksCreate(e *env, ctx *context) error { return nil } -func (h *functionHooksCmd) functionHooksRead(e *env, ctx *context) error { +func (h *functionHooksCmd) functionHooksRead(e *parsecli.Env, ctx *parsecli.Context) error { u := defaultFunctionsURL var function *functionHook if !h.All { @@ -136,7 +137,7 @@ func (h *functionHooksCmd) functionHooksRead(e *env, ctx *context) error { return nil } -func (h *functionHooksCmd) functionHooksUpdate(e *env, ctx *context) error { +func (h *functionHooksCmd) functionHooksUpdate(e *parsecli.Env, ctx *parsecli.Context) error { params, err := readFunctionParams(e, h.Function) if err != nil { return err @@ -163,7 +164,7 @@ func (h *functionHooksCmd) functionHooksUpdate(e *env, ctx *context) error { return nil } -func (h *functionHooksCmd) functionHooksDelete(e *env, ctx *context) error { +func (h *functionHooksCmd) functionHooksDelete(e *parsecli.Env, ctx *parsecli.Context) error { params, err := readFunctionName(e, h.Function) if err != nil { return err @@ -193,27 +194,27 @@ func (h *functionHooksCmd) functionHooksDelete(e *env, ctx *context) error { return nil } -func (h *functionHooksCmd) functionHooks(e *env, c *context) error { +func (h *functionHooksCmd) functionHooks(e *parsecli.Env, c *parsecli.Context) error { hp := *h hp.All = true return hp.functionHooksRead(e, c) } -func newFunctionHooksCmd(e *env) *cobra.Command { +func NewFunctionHooksCmd(e *parsecli.Env) *cobra.Command { h := &functionHooksCmd{interactive: true} c := &cobra.Command{ Use: "functions", Short: "List cloud code functions and function webhooks", Long: "List cloud code functions and function webhooks", - Run: runWithClient(e, h.functionHooks), + Run: parsecli.RunWithClient(e, h.functionHooks), } createCmd := &cobra.Command{ Use: "create", Short: "Create a function webhook", Long: "Create a function webhook", - Run: runWithClient(e, h.functionHooksCreate), + Run: parsecli.RunWithClient(e, h.functionHooksCreate), } c.AddCommand(createCmd) @@ -221,7 +222,7 @@ func newFunctionHooksCmd(e *env) *cobra.Command { Use: "edit", Short: "Edit the URL of a function webhook", Long: "Edit the URL of a function webhook", - Run: runWithClient(e, h.functionHooksUpdate), + Run: parsecli.RunWithClient(e, h.functionHooksUpdate), } c.AddCommand(changeCmd) @@ -229,7 +230,7 @@ func newFunctionHooksCmd(e *env) *cobra.Command { Use: "delete", Short: "Delete a function webhook", Long: "Delete a function webhook", - Run: runWithClient(e, h.functionHooksDelete), + Run: parsecli.RunWithClient(e, h.functionHooksDelete), } c.AddCommand(deleteCmd) diff --git a/functions_cmd_test.go b/functions_test.go similarity index 80% rename from functions_cmd_test.go rename to functions_test.go index 4823c3d..343ca79 100644 --- a/functions_cmd_test.go +++ b/functions_test.go @@ -9,16 +9,17 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/jsonpipe" "github.com/facebookgo/parse" ) -func newFunctionsHarness(t testing.TB) *Harness { - h := newHarness(t) +func newFunctionsHarness(t testing.TB) *parsecli.Harness { + h := parsecli.NewHarness(t) defer h.Stop() - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { var body interface{} var params map[string]interface{} if r.Body != nil { @@ -98,7 +99,7 @@ func newFunctionsHarness(t testing.TB) *Harness { Body: ioutil.NopCloser(jsonpipe.Encode(body)), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} return h } @@ -116,20 +117,20 @@ func TestFunctionHookString(t *testing.T) { func TestReadFuncParams(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.env.In = strings.NewReader("\n") - _, err := readFunctionName(h.env, nil) + h.Env.In = strings.NewReader("\n") + _, err := readFunctionName(h.Env, nil) ensure.Err(t, err, regexp.MustCompile("Function name cannot be empty")) - h.env.In = strings.NewReader("foo\n") - hook, err := readFunctionName(h.env, nil) + h.Env.In = strings.NewReader("foo\n") + hook, err := readFunctionName(h.Env, nil) ensure.Nil(t, err) ensure.DeepEqual(t, *hook, functionHook{FunctionName: "foo"}) - h.env.In = strings.NewReader("foo\napi.example.com/foo\n") - hook, err = readFunctionParams(h.env, nil) + h.Env.In = strings.NewReader("foo\napi.example.com/foo\n") + hook, err = readFunctionParams(h.Env, nil) ensure.Nil(t, err) ensure.DeepEqual(t, *hook, functionHook{ FunctionName: "foo", @@ -143,7 +144,7 @@ func TestFunctionHooksRead(t *testing.T) { h := newFunctionsHarness(t) f := functionHooksCmd{All: true} - ensure.Nil(t, f.functionHooksRead(h.env, nil)) + ensure.Nil(t, f.functionHooksRead(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `The following cloudcode or webhook functions are associated with this app: Function name: "bar", URL: "https://api.example.com/bar" Function name: "foo" @@ -151,9 +152,9 @@ Function name: "foo", URL: "https://api.example.com/foo" `) f.All = false - h.env.In = strings.NewReader("foo\napi.example.com/foo\n") + h.Env.In = strings.NewReader("foo\napi.example.com/foo\n") h.Out.Reset() - ensure.Nil(t, f.functionHooksRead(h.env, nil)) + ensure.Nil(t, f.functionHooksRead(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter the function name: The following functions named: "foo" are associated with your app: Function name: "foo" Function name: "foo", URL: "https://api.example.com/foo" @@ -166,8 +167,8 @@ func TestFunctionHooksCreate(t *testing.T) { h := newFunctionsHarness(t) var f functionHooksCmd - h.env.In = strings.NewReader("foo\napi.example.com/foo\n") - ensure.Nil(t, f.functionHooksCreate(h.env, nil)) + h.Env.In = strings.NewReader("foo\napi.example.com/foo\n") + ensure.Nil(t, f.functionHooksCreate(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter the function name: URL: https://Successfully created a webhook function "foo" pointing to "https://api.example.com/foo" @@ -177,8 +178,8 @@ func TestFunctionHooksCreate(t *testing.T) { h.Out.Reset() h.Err.Reset() - h.env.In = strings.NewReader("bar\napi.example.com/bar\n") - ensure.Nil(t, f.functionHooksCreate(h.env, nil)) + h.Env.In = strings.NewReader("bar\napi.example.com/bar\n") + ensure.Nil(t, f.functionHooksCreate(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter the function name: URL: https://Successfully created a webhook function "bar" pointing to "https://api.example.com/bar" @@ -192,8 +193,8 @@ func TestFunctionHooksUpdate(t *testing.T) { h := newFunctionsHarness(t) var f functionHooksCmd - h.env.In = strings.NewReader("foo\napi.example.com/_foo\n") - ensure.Nil(t, f.functionHooksUpdate(h.env, nil)) + h.Env.In = strings.NewReader("foo\napi.example.com/_foo\n") + ensure.Nil(t, f.functionHooksUpdate(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter the function name: URL: https://Successfully update the webhook function "foo" to point to "https://api.example.com/_foo" @@ -204,8 +205,8 @@ func TestFunctionHooksUpdate(t *testing.T) { h.Out.Reset() h.Err.Reset() - h.env.In = strings.NewReader("bar\napi.example.com/_bar\n") - ensure.Nil(t, f.functionHooksUpdate(h.env, nil)) + h.Env.In = strings.NewReader("bar\napi.example.com/_bar\n") + ensure.Nil(t, f.functionHooksUpdate(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter the function name: URL: https://Successfully update the webhook function "bar" to point to "https://api.example.com/_bar" @@ -220,8 +221,8 @@ func TestFunctionHookDelete(t *testing.T) { h := newFunctionsHarness(t) f := &functionHooksCmd{interactive: true} - h.env.In = strings.NewReader("foo\ny\n") - ensure.Nil(t, f.functionHooksDelete(h.env, nil)) + h.Env.In = strings.NewReader("foo\ny\n") + ensure.Nil(t, f.functionHooksDelete(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter the function name: Are you sure you want to delete webhook function: "foo" (y/n): Successfully deleted webhook function "foo" @@ -229,8 +230,8 @@ Function "foo" defined in cloud code will be used henceforth `) h.Out.Reset() - h.env.In = strings.NewReader("foo\nn\n") - ensure.Nil(t, f.functionHooksDelete(h.env, nil)) + h.Env.In = strings.NewReader("foo\nn\n") + ensure.Nil(t, f.functionHooksDelete(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter the function name: Are you sure you want to delete webhook function: "foo" (y/n): `) diff --git a/list_cmd.go b/list.go similarity index 67% rename from list_cmd.go rename to list.go index 156bd2c..b6b9d06 100644 --- a/list_cmd.go +++ b/list.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -14,8 +15,8 @@ const ( type listCmd struct{} -func (l *listCmd) printListOfApps(e *env) error { - config, err := configFromDir(e.Root) +func (l *listCmd) printListOfApps(e *parsecli.Env) error { + config, err := parsecli.ConfigFromDir(e.Root) if err != nil { if os.IsNotExist(err) { // print nothing if we are not in a parse app directory @@ -23,26 +24,26 @@ func (l *listCmd) printListOfApps(e *env) error { } return stackerr.Wrap(err) } - config.pprintApps(e) + config.PrettyPrintApps(e) fmt.Fprintln(e.Out, "") return nil } -func (l *listCmd) run(e *env, args []string) error { +func (l *listCmd) run(e *parsecli.Env, args []string) error { l.printListOfApps(e) - var apps apps - if err := apps.login.authUser(e); err != nil { + var apps parsecli.Apps + if err := apps.Login.AuthUser(e); err != nil { return err } var appName string if len(args) > 0 { appName = args[0] } - return apps.showApps(e, appName) + return apps.ShowApps(e, appName) } -func newListCmd(e *env) *cobra.Command { +func NewListCmd(e *parsecli.Env) *cobra.Command { l := listCmd{} return &cobra.Command{ Use: "list [app]", @@ -51,6 +52,6 @@ func newListCmd(e *env) *cobra.Command { when executed inside the directory. Additionally, it prints the list of Parse apps associated with current Parse account. If an optional app name is provided, it prints all the keys for that app.`, - Run: runWithArgs(e, l.run), + Run: parsecli.RunWithArgs(e, l.run), } } diff --git a/list_cmd_test.go b/list_test.go similarity index 51% rename from list_cmd_test.go rename to list_test.go index 7a1b644..5184606 100644 --- a/list_cmd_test.go +++ b/list_test.go @@ -5,82 +5,82 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) -func newListCmdHarness(t testing.TB) (*Harness, *listCmd) { - h := newHarness(t) +func newListCmdHarness(t testing.TB) (*parsecli.Harness, *listCmd) { + h := parsecli.NewHarness(t) l := &listCmd{} return h, l } func TestPrintListOneAppNoDefaultKey(t *testing.T) { t.Parallel() - conf := &parseConfig{Applications: map[string]*parseAppConfig{"first": {}}} - h := newHarness(t) + conf := &parsecli.ParseConfig{Applications: map[string]*parsecli.ParseAppConfig{"first": {}}} + h := parsecli.NewHarness(t) defer h.Stop() - conf.pprintApps(h.env) + conf.PrettyPrintApps(h.Env) ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with cloud code in the current directory:\n first\n") } func TestPrintListOneAppWithDefaultKey(t *testing.T) { t.Parallel() - conf := &parseConfig{Applications: map[string]*parseAppConfig{"first": {}}} - conf.Applications[defaultKey] = &parseAppConfig{Link: "first"} + conf := &parsecli.ParseConfig{Applications: map[string]*parsecli.ParseAppConfig{"first": {}}} + conf.Applications[parsecli.DefaultKey] = &parsecli.ParseAppConfig{Link: "first"} - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - conf.pprintApps(h.env) + conf.PrettyPrintApps(h.Env) ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with cloud code in the current directory:\n* first\n") } func TestPrintListTwoAppsWithDefaultKey(t *testing.T) { t.Parallel() - conf := &parseConfig{Applications: map[string]*parseAppConfig{"first": {}, "second": {}}} - conf.Applications[defaultKey] = &parseAppConfig{Link: "first"} + conf := &parsecli.ParseConfig{Applications: map[string]*parsecli.ParseAppConfig{"first": {}, "second": {}}} + conf.Applications[parsecli.DefaultKey] = &parsecli.ParseAppConfig{Link: "first"} - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - conf.pprintApps(h.env) + conf.PrettyPrintApps(h.Env) ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with cloud code in the current directory:\n* first\n second\n") } func TestPrintListTwoAppsWithLinks(t *testing.T) { t.Parallel() - conf := &parseConfig{Applications: map[string]*parseAppConfig{"first": {}, "second": {Link: "first"}}} - conf.Applications[defaultKey] = &parseAppConfig{Link: "first"} - h := newHarness(t) + conf := &parsecli.ParseConfig{Applications: map[string]*parsecli.ParseAppConfig{"first": {}, "second": {Link: "first"}}} + conf.Applications[parsecli.DefaultKey] = &parsecli.ParseAppConfig{Link: "first"} + h := parsecli.NewHarness(t) defer h.Stop() - conf.pprintApps(h.env) + conf.PrettyPrintApps(h.Env) ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with cloud code in the current directory:\n* first\n second -> first\n") } func TestPrintListNoConfig(t *testing.T) { t.Parallel() h, l := newListCmdHarness(t) - h.makeEmptyRoot() + h.MakeEmptyRoot() defer h.Stop() - ensure.NotNil(t, l.printListOfApps(h.env)) + ensure.NotNil(t, l.printListOfApps(h.Env)) ensure.DeepEqual(t, h.Out.String(), "") } func TestPrintListNoApps(t *testing.T) { t.Parallel() h, l := newListCmdHarness(t) - h.makeEmptyRoot() + h.MakeEmptyRoot() defer h.Stop() - n := &newCmd{} - n.cloneSampleCloudCode(h.env, true) - ensure.Nil(t, l.printListOfApps(h.env)) + parsecli.CloneSampleCloudCode(h.Env, true) + ensure.Nil(t, l.printListOfApps(h.Env)) } func TestRunListCmd(t *testing.T) { t.Parallel() - h, _ := newAppHarness(t) + h, _ := parsecli.NewAppHarness(t) defer h.Stop() l := &listCmd{} - h.env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) - ensure.Nil(t, l.run(h.env, []string{"A"})) + h.Env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) + ensure.Nil(t, l.run(h.Env, []string{"A"})) ensure.StringContains(t, h.Out.String(), `Properties of the app "A"`) } diff --git a/login_test.go b/login_test.go deleted file mode 100644 index 3ae1379..0000000 --- a/login_test.go +++ /dev/null @@ -1,242 +0,0 @@ -package main - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "regexp" - "strings" - "testing" - - "github.com/facebookgo/ensure" - "github.com/facebookgo/parse" -) - -func newTokenHarness(t testing.TB) *Harness { - h := newHarness(t) - ht := transportFunc(func(r *http.Request) (*http.Response, error) { - ensure.DeepEqual(t, r.URL.Path, "/1/accountkey") - ensure.DeepEqual(t, r.Method, "POST") - - key := &struct { - AccountKey string `json:"accountKey"` - }{} - ensure.Nil(t, json.NewDecoder(ioutil.NopCloser(r.Body)).Decode(key)) - - if key.AccountKey != "token" { - return &http.Response{ - StatusCode: http.StatusUnauthorized, - Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect token"}`)), - }, nil - } - - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser(strings.NewReader(`{"email": "email"}`)), - }, nil - }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - return h -} - -func TestPopulateCreds(t *testing.T) { - t.Parallel() - - h := newHarness(t) - defer h.Stop() - - l := &login{} - h.env.In = strings.NewReader("email\npassword\n") - ensure.Nil(t, l.populateCreds(h.env)) - ensure.DeepEqual(t, l.credentials.email, "email") - ensure.DeepEqual(t, l.credentials.password, "password") -} - -func TestGetTokenCredentials(t *testing.T) { - t.Parallel() - - h := newHarness(t) - defer h.Stop() - - l := &login{} - h.env.Server = "http://api.example.com/1/" - - l.tokenReader = strings.NewReader( - `machine api.example.com - login default - password token - `, - ) - _, credentials, err := l.getTokenCredentials(h.env, "") - ensure.Nil(t, err) - ensure.DeepEqual(t, credentials.token, "token") - - l.tokenReader = strings.NewReader( - `machine api.example.com - login default - password token - `, - ) - h.env.Server = "http://api.parse.com" - _, credentials, err = l.getTokenCredentials(h.env, "") - ensure.Err(t, err, keyNotFound) - - l = &login{} - h.env.Server = "http://api.example.com/1/" - - l.tokenReader = strings.NewReader( - `machine api.example.com#email - login default - password token - `, - ) - _, credentials, err = l.getTokenCredentials(h.env, "email") - ensure.Nil(t, err) - ensure.DeepEqual(t, credentials.token, "token") - - l.tokenReader = strings.NewReader( - `machine api.example.com#email - login default - password token - `, - ) - h.env.Server = "http://api.parse.com" - _, credentials, err = l.getTokenCredentials(h.env, "email") - ensure.Err(t, err, keyNotFound) - - l = &login{} - h.env.Server = "http://api.example.com/1/" - - l.tokenReader = strings.NewReader( - `machine api.example.com#email - login default - password token1 -machine api.example.com - login default - password token2 -`, - ) - _, credentials, err = l.getTokenCredentials(h.env, "email") - ensure.Nil(t, err) - ensure.DeepEqual(t, credentials.token, "token1") - - l.tokenReader = strings.NewReader( - `machine api.example.com#email - login default - password token1 -machine api.example.com - login default - password token2 -`, - ) - - _, credentials, err = l.getTokenCredentials(h.env, "xmail") - ensure.Nil(t, err) - ensure.DeepEqual(t, credentials.token, "token2") -} - -func TestAuthUserWithToken(t *testing.T) { - t.Parallel() - - h := newTokenHarness(t) - defer h.Stop() - - l := &login{} - h.env.ParserEmail = "email" - h.env.Server = "http://api.example.org/1/" - - l.tokenReader = strings.NewReader( - `machine api.example.org#email - login default - password token - `, - ) - _, err := l.authUserWithToken(h.env) - ensure.Nil(t, err) - - h.env.ParserEmail = "email2" - - l.tokenReader = strings.NewReader( - `machine api.example.org#email2 - login default - password token - `, - ) - _, err = l.authUserWithToken(h.env) - ensure.Err(t, err, regexp.MustCompile(`does not belong to "email2"`)) - - h.env.ParserEmail = "" - - l.tokenReader = strings.NewReader( - `machine api.example.org - login default - password token2 - `, - ) - _, err = l.authUserWithToken(h.env) - ensure.Err(t, err, regexp.MustCompile("provided is not valid")) - -} - -func TestUpdatedNetrcContent(t *testing.T) { - t.Parallel() - - h := newHarness(t) - defer h.Stop() - - l := &login{} - - h.env.Server = "https://api.example.com/1/" - updated, err := l.updatedNetrcContent( - h.env, - strings.NewReader( - `machine api.example.com#email - login default - password token0 - -machine api.example.org - login default - password token -`, - ), - "email", - &credentials{token: "token"}, - ) - - ensure.Nil(t, err) - ensure.DeepEqual(t, - string(updated), - `machine api.example.com#email - login default - password token - -machine api.example.org - login default - password token -`, - ) - - h.env.Server = "https://api.example.org/1/" - updated, err = l.updatedNetrcContent(h.env, - strings.NewReader( - `machine api.example.com - login default - password token -`, - ), - "email", - &credentials{token: "token"}, - ) - - ensure.Nil(t, err) - ensure.DeepEqual(t, - string(updated), - `machine api.example.com - login default - password token - -machine api.example.org#email - login default - password token`, - ) -} diff --git a/main.go b/main.go index 822c9bb..ea34e5a 100644 --- a/main.go +++ b/main.go @@ -2,38 +2,19 @@ package main import ( "fmt" - "io" + "net/http" + "net/url" "os" "os/signal" - "runtime" + "strings" + "time" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/clock" + "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) -const ( - version = "2.2.8" - cloudDir = "cloud" - hostingDir = "public" - defaultBaseURL = "https://api.parse.com/1/" -) - -var userAgent = fmt.Sprintf("parse-cli-%s-%s", runtime.GOOS, version) - -type env struct { - Root string // project root - Server string // parse api server - Type int // project type - ParserEmail string // email associated with developer parse account - ErrorStack bool - Out io.Writer - Err io.Writer - In io.Reader - Exit func(int) - Clock clock.Clock - ParseAPIClient *ParseAPIClient -} - func main() { // some parts of apps.go are unable to handle // interrupts, this logic ensures we exit on system interrupts @@ -44,7 +25,7 @@ func main() { os.Exit(1) }() - e := env{ + e := parsecli.Env{ Root: os.Getenv("PARSE_ROOT"), Server: os.Getenv("PARSE_SERVER"), ErrorStack: os.Getenv("PARSE_ERROR_STACK") == "1", @@ -61,33 +42,33 @@ func main() { fmt.Fprintf(e.Err, "Failed to get current directory:\n%s\n", err) os.Exit(1) } - root := getProjectRoot(&e, cur) - if isProjectDir(root) { + root := parsecli.GetProjectRoot(&e, cur) + if parsecli.IsProjectDir(root) { e.Root = root - config, err := configFromDir(root) + config, err := parsecli.ConfigFromDir(root) if err != nil { fmt.Fprintln(e.Err, err) os.Exit(1) } - e.Type = config.getProjectConfig().Type + e.Type = config.GetProjectConfig().Type if e.ParserEmail == "" { - e.ParserEmail = config.getProjectConfig().ParserEmail + e.ParserEmail = config.GetProjectConfig().ParserEmail } } else { - e.Type = legacyParseFormat - e.Root = getLegacyProjectRoot(&e, cur) + e.Type = parsecli.LegacyParseFormat + e.Root = parsecli.GetLegacyProjectRoot(&e, cur) } } - if e.Type != legacyParseFormat && e.Type != parseFormat { + if e.Type != parsecli.LegacyParseFormat && e.Type != parsecli.ParseFormat { fmt.Fprintf(e.Err, "Unknown project type %d.\n", e.Type) os.Exit(1) } if e.Server == "" { - e.Server = defaultBaseURL + e.Server = parsecli.DefaultBaseURL } - apiClient, err := newParseAPIClient(&e) + apiClient, err := parsecli.NewParseAPIClient(&e) if err != nil { fmt.Fprintln(e.Err, err) os.Exit(1) @@ -99,12 +80,12 @@ func main() { command []string ) switch e.Type { - case legacyParseFormat, parseFormat: + case parsecli.LegacyParseFormat, parsecli.ParseFormat: command, rootCmd = parseRootCmd(&e) } if len(command) == 0 || command[0] != "update" { - message, err := checkIfSupported(&e, version, command...) + message, err := checkIfSupported(&e, parsecli.Version, command...) if err != nil { fmt.Fprintln(e.Err, err) os.Exit(1) @@ -119,3 +100,35 @@ func main() { os.Exit(1) } } + +func checkIfSupported(e *parsecli.Env, version string, args ...string) (string, error) { + v := make(url.Values) + v.Set("version", version) + v.Set("other", strings.Join(args, " ")) + req := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, + } + + type result struct { + warning string + err error + } + + timeout := make(chan *result, 1) + go func() { + var res struct { + Warning string `json:"warning"` + } + _, err := e.ParseAPIClient.Do(req, nil, &res) + timeout <- &result{warning: res.Warning, err: err} + }() + + select { + case res := <-timeout: + return res.warning, stackerr.Wrap(res.err) + case <-time.After(time.Duration(500) * time.Millisecond): + return "", nil + } + return "", nil +} diff --git a/main_test.go b/main_test.go index 495e367..d5d831d 100644 --- a/main_test.go +++ b/main_test.go @@ -1,406 +1,73 @@ package main import ( - "bytes" - "encoding/json" - "errors" - "fmt" "io/ioutil" "net/http" - "os" - "path/filepath" "regexp" - "strings" "testing" - "github.com/facebookgo/clock" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" - "github.com/facebookgo/errgroup" + "github.com/facebookgo/jsonpipe" "github.com/facebookgo/parse" - "github.com/facebookgo/stackerr" - "github.com/facebookgo/testname" - "github.com/spf13/cobra" ) -func noOpCmd() *cobra.Command { - var c cobra.Command - c.SetOutput(ioutil.Discard) - return &c -} - -type Harness struct { - T testing.TB - Out bytes.Buffer - Err bytes.Buffer - Clock *clock.Mock - env *env - remove []string -} - -func (h *Harness) makeEmptyRoot() { - var err error - prefix := fmt.Sprintf("%s-", testname.Get("parse-cli-")) - h.env.Root, err = ioutil.TempDir("", prefix) - ensure.Nil(h.T, err) - h.remove = append(h.remove, h.env.Root) -} - -func (h *Harness) makeWithConfig(global string) { - h.env.Root = makeDirWithConfig(h.T, global) -} - -func (h *Harness) Stop() { - for _, p := range h.remove { - os.RemoveAll(p) - } -} - -func newHarness(t testing.TB) *Harness { - te := Harness{ - T: t, - Clock: clock.NewMock(), - } - te.env = &env{ - Out: &te.Out, - Err: &te.Err, - Clock: te.Clock, - ParseAPIClient: &ParseAPIClient{apiClient: &parse.Client{}}, - } - return &te -} - -// makes a temp directory with the given global config. -func makeDirWithConfig(t testing.TB, global string) string { - dir, err := ioutil.TempDir("", testname.Get("parse-cli-")) - ensure.Nil(t, err) - ensure.Nil(t, os.Mkdir(filepath.Join(dir, "config"), 0755)) - ensure.Nil(t, ioutil.WriteFile( - filepath.Join(dir, legacyConfigFile), - []byte(global), - os.FileMode(0600), - )) - return dir -} - -type transportFunc func(r *http.Request) (*http.Response, error) - -func (t transportFunc) RoundTrip(r *http.Request) (*http.Response, error) { - return t(r) -} - -func jsonStr(t testing.TB, v interface{}) string { - b, err := json.Marshal(v) - ensure.Nil(t, err) - return string(b) -} - -func TestVersion(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - var c versionCmd - err := c.run(h.env) - ensure.Nil(t, err) - ensure.DeepEqual(t, h.Out.String(), version+"\n") - ensure.DeepEqual(t, h.Err.String(), "") -} - -// this just exists to make code coverage less noisy. -func TestRootCommand(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - parseRootCmd(h.env) -} - -func TestErrorStringWithoutStack(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - h.env.ErrorStack = false - const message = "hello world" - actual := errorString(h.env, stackerr.New(message)) - ensure.StringContains(t, actual, message) - ensure.StringDoesNotContain(t, actual, ".go") -} - -type exitCode int - -func TestErrorStringWithStack(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - h.env.ErrorStack = true - const message = "hello world" - actual := errorString(h.env, stackerr.New(message)) - ensure.StringContains(t, actual, message) - ensure.StringContains(t, actual, ".go") -} - -func TestRunNoArgsWithArg(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - h.env.Exit = func(i int) { panic(exitCode(i)) } - func() { - defer ensure.PanicDeepEqual(t, exitCode(1)) - r := runNoArgs(h.env, nil) - r(noOpCmd(), []string{"foo"}) - }() - ensure.StringContains(t, h.Err.String(), "unexpected arguments") -} - -func TestRunNoArgsFuncError(t *testing.T) { - t.Parallel() - const message = "hello world" - h := newHarness(t) - defer h.Stop() - h.env.Exit = func(i int) { panic(exitCode(i)) } - func() { - defer ensure.PanicDeepEqual(t, exitCode(1)) - r := runNoArgs(h.env, func(*env) error { return errors.New(message) }) - r(noOpCmd(), nil) - }() - ensure.StringContains(t, h.Err.String(), message) -} - -func TestRunWithAppMultipleArgs(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - h.env.Exit = func(i int) { panic(exitCode(i)) } - func() { - defer ensure.PanicDeepEqual(t, exitCode(1)) - r := runWithClient(h.env, nil) - r(noOpCmd(), []string{"foo", "bar"}) - }() - ensure.StringContains( - t, - h.Err.String(), - "only an optional app name is expected", - ) -} - -func TestRunWithAppNonProjectDir(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - h.makeEmptyRoot() - h.env.Exit = func(i int) { panic(exitCode(i)) } - func() { - defer ensure.PanicDeepEqual(t, exitCode(1)) - r := runWithClient(h.env, nil) - r(noOpCmd(), nil) - }() - ensure.StringContains( - t, - h.Err.String(), - "Command must be run inside a Parse project.", - ) -} - -func TestRunWithAppNotFound(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - c := legacyConfig{Applications: map[string]*parseAppConfig{"a": {}}} - h.makeWithConfig(jsonStr(t, c)) - h.env.Exit = func(i int) { panic(exitCode(i)) } - func() { - defer ensure.PanicDeepEqual(t, exitCode(1)) - r := runWithClient(h.env, nil) - r(noOpCmd(), []string{"b"}) - }() - ensure.StringContains(t, h.Err.String(), `App "b" wasn't found`) -} - -func TestRunWithAppNamed(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - const appName = "a" - c := &parseConfig{ - Applications: map[string]*parseAppConfig{ - appName: {ApplicationID: "id", MasterKey: "token"}, - }, - } - h.makeWithConfig(jsonStr(t, c)) - r := runWithClient(h.env, func(e *env, c *context) error { - ensure.NotNil(t, c) - return nil - }) - r(noOpCmd(), []string{"a"}) -} - -func TestRunWithDefault(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - const appName = "a" - c := &parseConfig{ - Applications: map[string]*parseAppConfig{ - appName: {ApplicationID: "id", MasterKey: "token"}, - defaultKey: {Link: appName}, - }, - } - h.makeWithConfig(jsonStr(t, c)) - r := runWithClient(h.env, func(e *env, c *context) error { - ensure.NotNil(t, c) - return nil - }) - r(noOpCmd(), []string{"a"}) -} - -func TestRunWithAppError(t *testing.T) { - t.Parallel() - h := newHarness(t) - defer h.Stop() - const appName = "a" - c := &parseConfig{ - Applications: map[string]*parseAppConfig{ - appName: {ApplicationID: "id", MasterKey: "token"}, - }, - } - h.makeWithConfig(jsonStr(t, c)) - h.env.Exit = func(i int) { panic(exitCode(i)) } - const message = "hello world" - func() { - defer ensure.PanicDeepEqual(t, exitCode(1)) - r := runWithClient(h.env, func(e *env, c *context) error { - ensure.NotNil(t, c) - return errors.New(message) - }) - r(noOpCmd(), []string{"a"}) - }() - ensure.StringContains(t, h.Err.String(), message) -} - func TestNewClientInvalidServerURL(t *testing.T) { t.Parallel() - c, err := newParseAPIClient(&env{Server: ":"}) + c, err := parsecli.NewParseAPIClient(&parsecli.Env{Server: ":"}) ensure.True(t, c == nil) ensure.Err(t, err, regexp.MustCompile("invalid server URL")) } -func TestErrorString(t *testing.T) { +func TestIsSupportedWarning(t *testing.T) { t.Parallel() - h := newHarness(t) - defer h.Stop() - apiErr := &parse.Error{Code: -1, Message: "Error\nMessage"} - ensure.DeepEqual(t, `Error -Message`, - errorString(h.env, apiErr), - ) -} -func TestGetProjectRoot(t *testing.T) { - t.Parallel() - h := newHarness(t) - h.makeEmptyRoot() + h := parsecli.NewHarness(t) defer h.Stop() - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, "parse"), 0755)) - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, "parse", "config"), 0755)) - f, err := os.Create(filepath.Join(h.env.Root, "parse", legacyConfigFile)) + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.URL.Path, "/1/supported") + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser( + jsonpipe.Encode( + map[string]string{"warning": "please update"}, + ), + ), + }, nil + }) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + message, err := checkIfSupported(h.Env, "2.0.2") ensure.Nil(t, err) - defer f.Close() - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, "parse", "cloud"), 0755)) - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, "parse", "public"), 0755)) - ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, "parse", "cloud", "other", "config"), 0755)) - - ensure.DeepEqual(t, getLegacyProjectRoot(h.env, h.env.Root), h.env.Root) - - ensure.DeepEqual(t, getLegacyProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "config")), filepath.Join(h.env.Root, "parse")) - - ensure.DeepEqual(t, getLegacyProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "cloud")), filepath.Join(h.env.Root, "parse")) - - ensure.DeepEqual(t, getLegacyProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "public")), filepath.Join(h.env.Root, "parse")) - - ensure.DeepEqual(t, getLegacyProjectRoot(h.env, filepath.Join(h.env.Root, "parse", "cloud", "other")), filepath.Join(h.env.Root, "parse")) + ensure.DeepEqual(t, message, "please update") } -func TestStackErrorString(t *testing.T) { +func TestIsSupportedError(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - err := stackerr.New("error") - - h.env.ErrorStack = false - errStr := errorString(h.env, err) - - ensure.DeepEqual(t, errStr, "error") - - h.env.ErrorStack = true - errStr = errorString(h.env, err) - - ensure.StringContains(t, errStr, "error") - ensure.StringContains(t, errStr, ".go") - - err = stackerr.Wrap(&parse.Error{Message: "message", Code: 1}) - h.env.ErrorStack = false - errStr = errorString(h.env, err) - - ensure.DeepEqual(t, errStr, "message") - - h.env.ErrorStack = true - errStr = errorString(h.env, err) - - ensure.StringContains(t, errStr, `parse: api error with code=1 and message="message`) - ensure.StringContains(t, errStr, ".go") -} - -func TestMultiErrorString(t *testing.T) { - t.Parallel() - - h := newHarness(t) - defer h.Stop() - - err := errgroup.MultiError( - []error{ - stackerr.New("error"), - stackerr.Wrap(&parse.Error{Message: "message", Code: 1}), - }, - ) - - h.env.ErrorStack = false - errStr := errorString(h.env, err) - - ensure.DeepEqual(t, errStr, "multiple errors: error | message") - - h.env.ErrorStack = true - errStr = errorString(h.env, err) - - ensure.StringContains(t, errStr, "multiple errors") - ensure.StringContains(t, errStr, `parse: api error with code=1 and message="message"`) - ensure.StringContains(t, errStr, ".go") + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.URL.Path, "/1/supported") + return &http.Response{ + StatusCode: http.StatusBadRequest, + Body: ioutil.NopCloser( + jsonpipe.Encode( + map[string]string{"error": "not supported"}, + ), + ), + }, nil + }) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + _, err := checkIfSupported(h.Env, "2.0.2") + ensure.Err(t, err, regexp.MustCompile("not supported")) } -func TestRunWithContext(t *testing.T) { +// this just exists to make code coverage less noisy. +func TestRootCommand(t *testing.T) { t.Parallel() - - h := newHarness(t) - h.makeEmptyRoot() + h := parsecli.NewHarness(t) defer h.Stop() - - n := &newCmd{} - h.env.Type = parseFormat - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - _, err := n.setupSample(h.env, - "yolo", - &parseAppConfig{ - ApplicationID: "yolo-id", - MasterKey: "yoda", - }, - false, - false, - ) - ensure.Nil(t, err) - - dummy := func(e *env, c *context) error { - ensure.DeepEqual(t, c.AppConfig.getApplicationID(), "yolo-id") - return nil - } - runWithAppClient(h.env, dummy) + parseRootCmd(h.Env) } diff --git a/new_cmd.go b/new_cmd.go deleted file mode 100644 index 0f1cbe5..0000000 --- a/new_cmd.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/facebookgo/stackerr" -) - -func (n *newCmd) cloneSampleCloudCode(e *env, dumpTemplate bool) error { - err := os.MkdirAll(e.Root, 0755) - if err != nil { - return stackerr.Wrap(err) - } - - err = n.createConfigWithContent( - filepath.Join(e.Root, parseProject), - fmt.Sprintf( - `{ - "project_type" : %d, - "parse": {"jssdk":""} -}`, - parseFormat, - ), - ) - if err != nil { - return err - } - err = n.createConfigWithContent( - filepath.Join(e.Root, parseLocal), - "{}", - ) - if err != nil { - return err - } - - // no need to set up the template code - if !dumpTemplate { - return nil - } - - for _, info := range newProjectFiles { - sampleDir := filepath.Join(e.Root, info.dirname) - if _, err := os.Stat(sampleDir); err != nil { - if !os.IsNotExist(err) { - return stackerr.Wrap(err) - } - if err := os.Mkdir(sampleDir, 0755); err != nil { - return stackerr.Wrap(err) - } - } - - sampleFile := filepath.Join(sampleDir, info.filename) - if _, err := os.Stat(sampleFile); err != nil { - if os.IsNotExist(err) { - file, err := os.Create(sampleFile) - if err != nil && !os.IsExist(err) { - return stackerr.Wrap(err) - } - - defer file.Close() - - if _, err := file.WriteString(info.content); err != nil { - return stackerr.Wrap(err) - } - if err := file.Close(); err != nil { - return stackerr.Wrap(err) - } - } - } - } - return nil -} diff --git a/parse_commands.go b/parse_commands.go new file mode 100644 index 0000000..692a044 --- /dev/null +++ b/parse_commands.go @@ -0,0 +1,67 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/ParsePlatform/parse-cli/parsecmd" + "github.com/spf13/cobra" +) + +func parseRootCmd(e *parsecli.Env) ([]string, *cobra.Command) { + c := &cobra.Command{ + Use: "parse", + Long: fmt.Sprintf( + `Parse Command Line Interface +Version %v +Copyright %d Parse, Inc. +http://parse.com`, + parsecli.Version, + time.Now().Year(), + ), + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + c.AddCommand(parsecmd.NewAddCmd(e)) + c.AddCommand(NewConfigureCmd(e)) + c.AddCommand(NewDefaultCmd(e)) + c.AddCommand(parsecmd.NewDeployCmd(e)) + c.AddCommand(parsecmd.NewDevelopCmd(e)) + c.AddCommand(parsecmd.NewDownloadCmd(e)) + c.AddCommand(NewFunctionHooksCmd(e)) + c.AddCommand(parsecmd.NewGenerateCmd(e)) + c.AddCommand(parsecmd.NewJsSdkCmd(e)) + c.AddCommand(NewListCmd(e)) + c.AddCommand(parsecmd.NewLogsCmd(e)) + c.AddCommand(parsecmd.NewMigrateCmd(e)) + c.AddCommand(parsecmd.NewNewCmd(e)) + c.AddCommand(parsecmd.NewReleasesCmd(e)) + c.AddCommand(parsecmd.NewRollbackCmd(e)) + c.AddCommand(parsecmd.NewSymbolsCmd(e)) + c.AddCommand(NewTriggerHooksCmd(e)) + c.AddCommand(NewUpdateCmd(e)) + c.AddCommand(NewVersionCmd(e)) + + if len(os.Args) <= 1 { + return nil, c + } + + commands := []string{"help"} + for _, command := range c.Commands() { + commands = append(commands, command.Name()) + } + + args := make([]string, len(os.Args)-1) + copy(args, os.Args[1:]) + + if message := parsecli.MakeCorrections(commands, args); message != "" { + fmt.Fprintln(e.Out, message) + } + c.SetArgs(args) + + return args, c +} diff --git a/apps.go b/parsecli/apps.go similarity index 83% rename from apps.go rename to parsecli/apps.go index 183ebde..e93f1bb 100644 --- a/apps.go +++ b/parsecli/apps.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "bytes" @@ -17,7 +17,7 @@ import ( "github.com/facebookgo/stackerr" ) -type app struct { +type App struct { Name string `json:"appName"` DashboardURL string `json:"dashboardURL"` ApplicationID string `json:"applicationId"` @@ -32,14 +32,13 @@ type app struct { RequireRevocableSessions bool `json:"requireRevocableSessions"` RevokeSessionOnPasswordChange bool `json:"revokeSessionOnPasswordChange"` } - -type apps struct { - login login +type Apps struct { + Login Login } const numRetries = 3 -func allApps(apps []*app) []string { +func allApps(apps []*App) []string { var appNames []string for _, app := range apps { appNames = append(appNames, app.Name) @@ -56,7 +55,7 @@ func selectionString(appNames []string) string { return buffer.String() } -func (a *apps) printApp(e *env, params *app) error { +func (a *Apps) PrintApp(e *Env, params *App) error { fmt.Fprintf(e.Out, "Properties of the app %q:\n", params.Name) w := new(tabwriter.Writer) @@ -78,33 +77,33 @@ func (a *apps) printApp(e *env, params *app) error { return stackerr.Wrap(w.Flush()) } -func getAuthHeaders(credentials *credentials, headers http.Header) http.Header { +func getAuthHeaders(credentials *Credentials, headers http.Header) http.Header { if headers == nil { headers = make(http.Header) } - if credentials.token != "" { - headers.Add("X-Parse-Account-Key", credentials.token) + if credentials.Token != "" { + headers.Add("X-Parse-Account-Key", credentials.Token) } else { - headers.Add("X-Parse-Email", credentials.email) - headers.Add("X-Parse-Password", credentials.password) + headers.Add("X-Parse-Email", credentials.Email) + headers.Add("X-Parse-Password", credentials.Password) } return headers } -func fetchAppKeys(e *env, appID string) (*app, error) { - l := &login{} - err := l.authUser(e) +func fetchAppKeys(e *Env, appID string) (*App, error) { + l := &Login{} + err := l.AuthUser(e) if err != nil { return nil, err } - credentials := &l.credentials + credentials := &l.Credentials req := &http.Request{ Method: "GET", URL: &url.URL{Path: path.Join("apps", appID)}, Header: getAuthHeaders(credentials, nil), } - res := &app{} + res := &App{} if response, err := e.ParseAPIClient.Do(req, nil, res); err != nil { if response.StatusCode == http.StatusUnauthorized { @@ -118,15 +117,15 @@ func fetchAppKeys(e *env, appID string) (*app, error) { return res, nil } -func (a *apps) restFetchApps(e *env) ([]*app, error) { +func (a *Apps) RestFetchApps(e *Env) ([]*App, error) { req := &http.Request{ Method: "GET", URL: &url.URL{Path: "apps"}, - Header: getAuthHeaders(&a.login.credentials, nil), + Header: getAuthHeaders(&a.Login.Credentials, nil), } var res struct { - Results []*app `json:"results"` + Results []*App `json:"results"` } if response, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { @@ -138,7 +137,7 @@ func (a *apps) restFetchApps(e *env) ([]*app, error) { return res.Results, nil } -func (a *apps) selectApp(apps []*app, msg string, e *env) (*app, error) { +func (a *Apps) SelectApp(apps []*App, msg string, e *Env) (*App, error) { appNames := allApps(apps) appCount := len(apps) @@ -174,15 +173,15 @@ func (a *apps) selectApp(apps []*app, msg string, e *env) (*app, error) { return nil, stackerr.Newf("Please try again. Please select from among the listed apps.") } -func (a *apps) showApps(e *env, appName string) error { - apps, err := a.restFetchApps(e) +func (a *Apps) ShowApps(e *Env, appName string) error { + apps, err := a.RestFetchApps(e) if err != nil { return err } if appName != "" { for _, app := range apps { if app.Name == appName { - return a.printApp(e, app) + return a.PrintApp(e, app) } } } @@ -195,7 +194,7 @@ func (a *apps) showApps(e *env, appName string) error { return nil } -func (a *apps) getAppName(e *env) (string, error) { +func (a *Apps) getAppName(e *Env) (string, error) { var appName string fmt.Fprint(e.Out, `Please choose a name for your Parse app. Note that this name will appear on the Parse website, @@ -208,15 +207,15 @@ Name: `) return appName, nil } -func (a *apps) restCreateApp(e *env, appName string) (*app, error) { +func (a *Apps) restCreateApp(e *Env, appName string) (*App, error) { req := &http.Request{ Method: "POST", URL: &url.URL{Path: "apps"}, - Header: getAuthHeaders(&a.login.credentials, nil), + Header: getAuthHeaders(&a.Login.Credentials, nil), Body: ioutil.NopCloser(jsonpipe.Encode(map[string]string{"appName": appName})), } - var res app + var res App if response, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { if response != nil && response.StatusCode == http.StatusUnauthorized { return nil, errAuth @@ -227,8 +226,8 @@ func (a *apps) restCreateApp(e *env, appName string) (*app, error) { return &res, nil } -func (a *apps) createApp(e *env, givenName string, retries int) (*app, error) { - apps, err := a.restFetchApps(e) +func (a *Apps) CreateApp(e *Env, givenName string, retries int) (*App, error) { + apps, err := a.RestFetchApps(e) if err != nil { return nil, err } diff --git a/parsecli/apps_test.go b/parsecli/apps_test.go new file mode 100644 index 0000000..364b6d9 --- /dev/null +++ b/parsecli/apps_test.go @@ -0,0 +1,199 @@ +package parsecli + +import ( + "errors" + "io/ioutil" + "net/http" + "regexp" + "strings" + "testing" + + "github.com/facebookgo/ensure" + "github.com/facebookgo/parse" +) + +var ( + defaultCredentials = Credentials{Email: "email", Password: "password"} + defaultTokenCredentials = Credentials{Email: "email", Token: "token"} + defaultApps = Apps{Login: Login{Credentials: defaultCredentials}} + defaultAppsWithToken = Apps{Login: Login{Credentials: defaultTokenCredentials}} +) + +func TestFetchAppKeys(t *testing.T) { + t.Parallel() + + h, _ := NewAppHarness(t) + defer h.Stop() + + h.Env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) + app, err := fetchAppKeys(h.Env, "an-app") + ensure.Nil(t, err) + ensure.DeepEqual(t, app, newTestApp("an-app")) +} + +func TestFetchApps(t *testing.T) { + t.Parallel() + + h, apps := NewAppHarness(t) + defer h.Stop() + + a := defaultApps + gotApps, err := a.RestFetchApps(h.Env) + ensure.Nil(t, err) + ensure.DeepEqual(t, gotApps, apps) + + a.Login.Credentials.Password = "invalid" + _, err = a.RestFetchApps(h.Env) + ensure.DeepEqual(t, err, errAuth) +} + +func TestFetchAppsToken(t *testing.T) { + t.Parallel() + + h, apps := NewAppHarness(t) + defer h.Stop() + + a := defaultAppsWithToken + gotApps, err := a.RestFetchApps(h.Env) + ensure.Nil(t, err) + ensure.DeepEqual(t, gotApps, apps) + + a.Login.Credentials.Token = "invalid" + _, err = a.RestFetchApps(h.Env) + ensure.DeepEqual(t, err, errAuth) +} + +func TestSelectAppWithRetries(t *testing.T) { + t.Parallel() + + h, apps := NewAppHarness(t) + + defer h.Stop() + + a := defaultApps + h.Env.In = ioutil.NopCloser(strings.NewReader("3\n1")) + selectedApp, err := a.SelectApp(apps, "test: ", h.Env) + ensure.Nil(t, err) + ensure.DeepEqual(t, h.Out.String(), + `1: A +2: B +test: Invalid selection 3: must be between 1 and 2 +1: A +2: B +test: `) + ensure.DeepEqual(t, selectedApp, newTestApp("A")) +} + +func TestSelectApp(t *testing.T) { + t.Parallel() + + // apps = [B, A] & appNames=[A,B] + h, apps := NewAppHarness(t) + apps[0], apps[1] = apps[1], apps[0] + + defer h.Stop() + + a := defaultApps + h.Env.In = ioutil.NopCloser(strings.NewReader("1")) + selectedApp, err := a.SelectApp(apps, "test: ", h.Env) + ensure.Nil(t, err) + ensure.DeepEqual(t, h.Out.String(), + `1: A +2: B +test: `) + ensure.DeepEqual(t, selectedApp, newTestApp("A")) +} + +func TestGetAppName(t *testing.T) { + t.Parallel() + + h := NewHarness(t) + defer h.Stop() + + a := defaultApps + h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) + appName, err := a.getAppName(h.Env) + ensure.Err(t, err, regexp.MustCompile("App name cannot be empty")) + + h.Env.In = ioutil.NopCloser(strings.NewReader("hello\n")) + appName, err = a.getAppName(h.Env) + ensure.Nil(t, err) + ensure.DeepEqual(t, appName, "hello") +} + +func TestCreateApp(t *testing.T) { + t.Parallel() + + h, _ := NewAppHarness(t) + defer h.Stop() + + a := defaultApps + app, err := a.restCreateApp(h.Env, "C") + + ensure.Nil(t, err) + ensure.DeepEqual(t, app, newTestApp("C")) +} + +func TestCreateNewApp(t *testing.T) { + t.Parallel() + + h, _ := NewAppHarness(t) + defer h.Stop() + + a := defaultApps + h.Env.In = ioutil.NopCloser(strings.NewReader("D")) + app, err := a.CreateApp(h.Env, "", 0) + ensure.Nil(t, err) + + ensure.Nil(t, a.PrintApp(h.Env, app)) + ensure.DeepEqual(t, h.Out.String(), + `Please choose a name for your Parse app. +Note that this name will appear on the Parse website, +but it does not have to be the same as your mobile app's public name. +Name: Properties of the app "D": +Name D +DashboardURL https://api.example.com/dashboard/D +ApplicationID applicationID.D +ClientKey clientKey.D +JavaScriptKey javaScriptKey.D +WindowsKey windowsKey.D +WebhookKey webhookKey.D +RestKey restKey.D +MasterKey masterKey.D +ClientPushEnabled false +ClientClassCreationEnabled true +RequireRevocableSessions true +RevokeSessionOnPasswordChange true +`) +} + +func TestCreateNewAppNameTaken(t *testing.T) { + t.Parallel() + + h, _ := NewAppHarness(t) + defer h.Stop() + + a := defaultApps + + h.Env.In = ioutil.NopCloser(strings.NewReader("A\nD")) + _, err := a.CreateApp(h.Env, "", 0) + ensure.Nil(t, err) + ensure.StringContains(t, h.Err.String(), "already created an app") +} + +func TestNoPanicAppCreate(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + + ht := TransportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.URL.Path, "/1/apps") + return nil, errors.New("nil response panic") + }) + h.Env.ParseAPIClient = &ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + + var apps Apps + app, err := apps.restCreateApp(h.Env, "panic!") + ensure.True(t, app == nil) + ensure.Err(t, err, regexp.MustCompile("nil response panic")) +} diff --git a/auto_correct.go b/parsecli/auto_correct.go similarity index 96% rename from auto_correct.go rename to parsecli/auto_correct.go index 8cffaa9..5974429 100644 --- a/auto_correct.go +++ b/parsecli/auto_correct.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "bytes" @@ -108,7 +108,7 @@ func SuggestCommands(given string, available []string) []string { // MakeCorrections can be used to automatically correct the incorrect command in input args // and return either an action that was taken or a suggestions message. // One can also provide a map of aliases and related conversions -func makeCorrections(commands []string, args []string) string { +func MakeCorrections(commands []string, args []string) string { pos := getArgPos(args) if pos == -1 { return "" diff --git a/auto_correct_test.go b/parsecli/auto_correct_test.go similarity index 73% rename from auto_correct_test.go rename to parsecli/auto_correct_test.go index 5087a26..3b30a67 100644 --- a/auto_correct_test.go +++ b/parsecli/auto_correct_test.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "testing" @@ -37,22 +37,22 @@ func TestCmdScoreLess(t *testing.T) { ensure.False(t, scores.Less(0, 1)) } -func TestmakeCorrections(t *testing.T) { +func TestMakeCorrections(t *testing.T) { t.Parallel() - ensure.DeepEqual(t, makeCorrections(nil, nil), "") - ensure.DeepEqual(t, makeCorrections(nil, []string{"arg"}), "") + ensure.DeepEqual(t, MakeCorrections(nil, nil), "") + ensure.DeepEqual(t, MakeCorrections(nil, []string{"arg"}), "") subCommands := []string{"cmd"} - ensure.DeepEqual(t, makeCorrections(subCommands, nil), "") + ensure.DeepEqual(t, MakeCorrections(subCommands, nil), "") - ensure.DeepEqual(t, makeCorrections(subCommands, []string{"--file"}), "") - ensure.DeepEqual(t, makeCorrections(subCommands, []string{"-f"}), "") - ensure.DeepEqual(t, makeCorrections(subCommands, []string{"--file", "-p"}), "") - ensure.DeepEqual(t, makeCorrections(subCommands, []string{"-p", "--file"}), "") + ensure.DeepEqual(t, MakeCorrections(subCommands, []string{"--file"}), "") + ensure.DeepEqual(t, MakeCorrections(subCommands, []string{"-f"}), "") + ensure.DeepEqual(t, MakeCorrections(subCommands, []string{"--file", "-p"}), "") + ensure.DeepEqual(t, MakeCorrections(subCommands, []string{"-p", "--file"}), "") args := []string{"--flags", "cmd", "args"} - ensure.DeepEqual(t, makeCorrections(subCommands, args), "") + ensure.DeepEqual(t, MakeCorrections(subCommands, args), "") ensure.DeepEqual(t, args, []string{"--flags", "cmd", "args"}) } @@ -92,7 +92,7 @@ func TestMatchesForSubCommands(t *testing.T) { } for _, testCase := range testCases { - ensure.DeepEqual(t, makeCorrections(subCommands, testCase.args), testCase.message) + ensure.DeepEqual(t, MakeCorrections(subCommands, testCase.args), testCase.message) ensure.DeepEqual(t, testCase.args, testCase.modArgs) } @@ -110,6 +110,6 @@ func TestMatchesForSubCommands(t *testing.T) { }, } for _, errorCase := range errorCases { - ensure.DeepEqual(t, makeCorrections(subCommands, errorCase.args), "") + ensure.DeepEqual(t, MakeCorrections(subCommands, errorCase.args), "") } } diff --git a/client.go b/parsecli/client.go similarity index 87% rename from client.go rename to parsecli/client.go index 7d99a3c..f0bd35e 100644 --- a/client.go +++ b/parsecli/client.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "net/http" @@ -10,16 +10,16 @@ import ( // ParseAPIClient is the http client used by parse-cli type ParseAPIClient struct { - apiClient *parse.Client + APIClient *parse.Client } -func newParseAPIClient(e *env) (*ParseAPIClient, error) { +func NewParseAPIClient(e *Env) (*ParseAPIClient, error) { baseURL, err := url.Parse(e.Server) if err != nil { return nil, stackerr.Newf("invalid server URL %q: %s", e.Server, err) } return &ParseAPIClient{ - apiClient: &parse.Client{ + APIClient: &parse.Client{ BaseURL: baseURL, }, }, nil @@ -29,7 +29,7 @@ func (c *ParseAPIClient) appendCommonHeaders(header http.Header) http.Header { if header == nil { header = make(http.Header) } - header.Add("User-Agent", userAgent) + header.Add("User-Agent", UserAgent) return header } @@ -60,17 +60,17 @@ func (c *ParseAPIClient) Delete(u *url.URL, result interface{}) (*http.Response, // RoundTrip is a wrapper for parse.Client.RoundTrip func (c *ParseAPIClient) RoundTrip(req *http.Request) (*http.Response, error) { req.Header = c.appendCommonHeaders(req.Header) - return c.apiClient.RoundTrip(req) + return c.APIClient.RoundTrip(req) } // Do is a wrapper for parse.Client.Do func (c *ParseAPIClient) Do(req *http.Request, body, result interface{}) (*http.Response, error) { req.Header = c.appendCommonHeaders(req.Header) - return c.apiClient.Do(req, body, result) + return c.APIClient.Do(req, body, result) } // WithCredentials is a wrapper for parse.Client.WithCredentials func (c *ParseAPIClient) WithCredentials(cr parse.Credentials) *ParseAPIClient { - c.apiClient = c.apiClient.WithCredentials(cr) + c.APIClient = c.APIClient.WithCredentials(cr) return c } diff --git a/config.go b/parsecli/config.go similarity index 51% rename from config.go rename to parsecli/config.go index a598225..c1ce8bd 100644 --- a/config.go +++ b/parsecli/config.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "encoding/json" @@ -11,76 +11,76 @@ import ( ) const ( - legacyParseFormat = iota - parseFormat + LegacyParseFormat = iota + ParseFormat - defaultKey = "_default" - parseLocal = ".parse.local" - parseProject = ".parse.project" + ParseLocal = ".parse.local" + ParseProject = ".parse.project" + DefaultKey = "_default" ) -type appConfig interface { - getApplicationID() string - getMasterKey(e *env) (string, error) - getApplicationAuth(e *env) (string, error) - getLink() string +type AppConfig interface { + GetApplicationID() string + GetMasterKey(e *Env) (string, error) + GetApplicationAuth(e *Env) (string, error) + GetLink() string } -type config interface { - app(string) (appConfig, error) - addAlias(string, string) error - setDefaultApp(string) error +type Config interface { + App(string) (AppConfig, error) + AddAlias(string, string) error + SetDefaultApp(string) error - getProjectConfig() *projectConfig - getDefaultApp() string - getNumApps() int - pprintApps(*env) + GetProjectConfig() *ProjectConfig + GetDefaultApp() string + GetNumApps() int + PrettyPrintApps(*Env) } -type projectConfig struct { +type ProjectConfig struct { // Type indicates the type of config. // Some examples are legacy, parse, etc. Type int `json:"project_type,omitempty"` // Parse stores the project config for Parse type. // Currently jssdk version is the only // project level config for Parse type. - Parse *parseProjectConfig `json:"parse,omitempty"` + Parse *ParseProjectConfig `json:"parse,omitempty"` // ParserEmail is an email id of the Parse developer. // It is associated with this project. // It is used to fetch appropriate credentials from netrc. ParserEmail string `json:"email,omitempty"` } -func getConfigFile(e *env) string { +func GetConfigFile(e *Env) string { switch e.Type { - case legacyParseFormat: - return filepath.Join(e.Root, legacyConfigFile) - case parseFormat: - return filepath.Join(e.Root, parseLocal) + case LegacyParseFormat: + return filepath.Join(e.Root, LegacyConfigFile) + case ParseFormat: + return filepath.Join(e.Root, ParseLocal) } return "" } -func configFromDir(dir string) (config, error) { - l, err := readLegacyConfigFile(filepath.Join(dir, legacyConfigFile)) +func ConfigFromDir(dir string) (Config, error) { + l, err := readLegacyConfigFile(filepath.Join(dir, LegacyConfigFile)) if err != nil && !stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) { return nil, err } if l != nil { // legacy config format - projectConfig := &projectConfig{ - Type: legacyParseFormat, - Parse: &parseProjectConfig{ + projectConfig := &ProjectConfig{ + Type: LegacyParseFormat, + Parse: &ParseProjectConfig{ JSSDK: l.Global.ParseVersion, }, ParserEmail: l.Global.ParserEmail, } applications := l.Applications if applications == nil { - applications = make(map[string]*parseAppConfig) + applications = make(map[string]*ParseAppConfig) } - return &parseConfig{ + return &ParseConfig{ Applications: applications, - projectConfig: projectConfig, + ProjectConfig: projectConfig, }, nil } @@ -95,28 +95,28 @@ func configFromDir(dir string) (config, error) { } // current config format - p, err := readProjectConfigFile(filepath.Join(dir, parseProject)) + p, err := readProjectConfigFile(filepath.Join(dir, ParseProject)) if err != nil { return nil, canonicalize(err) } - configFile := filepath.Join(dir, parseLocal) + configFile := filepath.Join(dir, ParseLocal) switch p.Type { - case parseFormat: + case ParseFormat: c, err := readParseConfigFile(configFile) if err != nil { return nil, canonicalize(err) } if c.Applications == nil { - c.Applications = make(map[string]*parseAppConfig) + c.Applications = make(map[string]*ParseAppConfig) } - c.projectConfig = p + c.ProjectConfig = p return c, nil } return nil, stackerr.Newf("Unknown project type: %d.", p.Type) } -func writeConfigFile(c config, path string) error { +func writeConfigFile(c Config, path string) error { b, err := json.MarshalIndent(c, "", " ") if err != nil { return stackerr.Wrap(err) @@ -124,40 +124,40 @@ func writeConfigFile(c config, path string) error { return stackerr.Wrap(ioutil.WriteFile(path, b, 0600)) } -func storeConfig(e *env, c config) error { - projectType := c.getProjectConfig().Type +func StoreConfig(e *Env, c Config) error { + projectType := c.GetProjectConfig().Type switch projectType { - case legacyParseFormat: - pc, ok := (c).(*parseConfig) + case LegacyParseFormat: + pc, ok := (c).(*ParseConfig) if !ok { return stackerr.Newf("Incorrect project type: 'legacy'.") } lconf := &legacyConfig{Applications: pc.Applications} - lconf.Global.ParseVersion = pc.projectConfig.Parse.JSSDK + lconf.Global.ParseVersion = pc.ProjectConfig.Parse.JSSDK return writeLegacyConfigFile( lconf, - filepath.Join(e.Root, legacyConfigFile), + filepath.Join(e.Root, LegacyConfigFile), ) - case parseFormat: - return writeConfigFile(c, filepath.Join(e.Root, parseLocal)) + case ParseFormat: + return writeConfigFile(c, filepath.Join(e.Root, ParseLocal)) } return stackerr.Newf("Unknown project type: %d.", projectType) } -func readProjectConfigFile(path string) (*projectConfig, error) { +func readProjectConfigFile(path string) (*ProjectConfig, error) { f, err := os.Open(path) if err != nil { return nil, stackerr.Wrap(err) } defer f.Close() - var p projectConfig + var p ProjectConfig if err := json.NewDecoder(f).Decode(&p); err != nil { return nil, stackerr.Newf("Config file %q is not valid JSON.", path) } return &p, err } -func writeProjectConfig(p *projectConfig, path string) error { +func writeProjectConfig(p *ProjectConfig, path string) error { b, err := json.MarshalIndent(p, "", " ") if err != nil { return stackerr.Wrap(err) @@ -165,43 +165,43 @@ func writeProjectConfig(p *projectConfig, path string) error { return stackerr.Wrap(ioutil.WriteFile(path, b, 0600)) } -func storeProjectConfig(e *env, c config) error { - p := c.getProjectConfig() +func StoreProjectConfig(e *Env, c Config) error { + p := c.GetProjectConfig() switch p.Type { - case legacyParseFormat: - p, ok := (c).(*parseConfig) + case LegacyParseFormat: + p, ok := (c).(*ParseConfig) if !ok { return stackerr.New("Invalid Cloud Code config.") } lconf := &legacyConfig{Applications: p.Applications} - lconf.Global.ParseVersion = p.projectConfig.Parse.JSSDK - lconf.Global.ParserEmail = p.projectConfig.ParserEmail + lconf.Global.ParseVersion = p.ProjectConfig.Parse.JSSDK + lconf.Global.ParserEmail = p.ProjectConfig.ParserEmail return writeLegacyConfigFile( lconf, - filepath.Join(e.Root, legacyConfigFile), + filepath.Join(e.Root, LegacyConfigFile), ) - case parseFormat: - return writeProjectConfig(p, filepath.Join(e.Root, parseProject)) + case ParseFormat: + return writeProjectConfig(p, filepath.Join(e.Root, ParseProject)) } return stackerr.Newf("Unknown project type: %d.", p.Type) } -func isProjectDir(cur string) bool { - _, err := os.Stat(filepath.Join(cur, parseProject)) +func IsProjectDir(cur string) bool { + _, err := os.Stat(filepath.Join(cur, ParseProject)) if err != nil { return false } - _, err = os.Stat(filepath.Join(cur, parseLocal)) + _, err = os.Stat(filepath.Join(cur, ParseLocal)) if err != nil { return false } return true } -func getProjectRoot(e *env, cur string) string { - if isProjectDir(cur) { +func GetProjectRoot(e *Env, cur string) string { + if IsProjectDir(cur) { return cur } @@ -211,7 +211,7 @@ func getProjectRoot(e *env, cur string) string { for base != "." && base != string(filepath.Separator) { base = filepath.Base(root) root = filepath.Dir(root) - if isProjectDir(root) { + if IsProjectDir(root) { return root } } @@ -219,7 +219,7 @@ func getProjectRoot(e *env, cur string) string { return cur } -func printDefault(e *env, defaultApp string) error { +func PrintDefault(e *Env, defaultApp string) error { if defaultApp == "" { return stackerr.New("No app is set as default app") } @@ -227,11 +227,11 @@ func printDefault(e *env, defaultApp string) error { return nil } -func setDefault(e *env, newDefault, defaultApp string, c config) error { - projectType := c.getProjectConfig().Type +func SetDefault(e *Env, newDefault, defaultApp string, c Config) error { + projectType := c.GetProjectConfig().Type switch projectType { - case legacyParseFormat, parseFormat: - p, ok := c.(*parseConfig) + case LegacyParseFormat, ParseFormat: + p, ok := c.(*ParseConfig) if !ok { return stackerr.New("Invalid Cloud Code config.") } @@ -240,3 +240,18 @@ func setDefault(e *env, newDefault, defaultApp string, c config) error { return stackerr.Newf("Unknown project type: %d.", projectType) } + +func SetParserEmail(e *Env, email string) error { + config, err := ConfigFromDir(e.Root) + if err != nil { + return err + } + config.GetProjectConfig().ParserEmail = email + err = StoreProjectConfig(e, config) + if err != nil { + fmt.Fprintln(e.Err, "Could not set parser email for project.") + return err + } + fmt.Fprintf(e.Out, "Successfully configured email for current project to: %q\n", email) + return nil +} diff --git a/parsecli/config_test.go b/parsecli/config_test.go new file mode 100644 index 0000000..968ed71 --- /dev/null +++ b/parsecli/config_test.go @@ -0,0 +1,72 @@ +package parsecli + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/facebookgo/ensure" +) + +func TestGetConfigFile(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + + h.Env.Type = LegacyParseFormat + ensure.DeepEqual(t, GetConfigFile(h.Env), filepath.Join(h.Env.Root, LegacyConfigFile)) + + h.Env.Type = ParseFormat + ensure.DeepEqual(t, GetConfigFile(h.Env), filepath.Join(h.Env.Root, ParseLocal)) +} + +func TestConfigFromDirFormatType(t *testing.T) { + t.Parallel() + h := NewHarness(t) + h.MakeEmptyRoot() + defer h.Stop() + + ensure.Nil(t, CloneSampleCloudCode(h.Env, true)) + + c, err := ConfigFromDir(h.Env.Root) + ensure.Nil(t, err) + + ensure.DeepEqual(t, c.GetProjectConfig().Type, ParseFormat) + + ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, ConfigDir), 0755)) + ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.Env.Root, LegacyConfigFile), + []byte("{}"), + 0600), + ) + c, err = ConfigFromDir(h.Env.Root) + ensure.Nil(t, err) + + ensure.DeepEqual(t, c.GetProjectConfig().Type, LegacyParseFormat) +} + +func TestGetProjectRoot(t *testing.T) { + t.Parallel() + h := NewHarness(t) + h.MakeEmptyRoot() + defer h.Stop() + + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse"), 0755)) + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse", "config"), 0755)) + f, err := os.Create(filepath.Join(h.Env.Root, "parse", LegacyConfigFile)) + ensure.Nil(t, err) + defer f.Close() + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse", "cloud"), 0755)) + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse", "public"), 0755)) + ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, "parse", "cloud", "other", "config"), 0755)) + + ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, h.Env.Root), h.Env.Root) + + ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, filepath.Join(h.Env.Root, "parse", "config")), filepath.Join(h.Env.Root, "parse")) + + ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, filepath.Join(h.Env.Root, "parse", "cloud")), filepath.Join(h.Env.Root, "parse")) + + ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, filepath.Join(h.Env.Root, "parse", "public")), filepath.Join(h.Env.Root, "parse")) + + ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, filepath.Join(h.Env.Root, "parse", "cloud", "other")), filepath.Join(h.Env.Root, "parse")) +} diff --git a/context.go b/parsecli/context.go similarity index 53% rename from context.go rename to parsecli/context.go index a773706..03067c4 100644 --- a/context.go +++ b/parsecli/context.go @@ -1,36 +1,36 @@ -package main +package parsecli import "github.com/facebookgo/parse" -type context struct { - Config config +type Context struct { + Config Config AppName string - AppConfig appConfig + AppConfig AppConfig } -func newContext(e *env, appName string) (*context, error) { - config, err := configFromDir(e.Root) +func newContext(e *Env, appName string) (*Context, error) { + config, err := ConfigFromDir(e.Root) if err != nil { return nil, err } - app, err := config.app(appName) + app, err := config.App(appName) if err != nil { return nil, err } - masterKey, err := app.getMasterKey(e) + masterKey, err := app.GetMasterKey(e) if err != nil { return nil, err } e.ParseAPIClient = e.ParseAPIClient.WithCredentials( parse.MasterKey{ - ApplicationID: app.getApplicationID(), + ApplicationID: app.GetApplicationID(), MasterKey: masterKey, }, ) - return &context{ + return &Context{ AppName: appName, AppConfig: app, Config: config, diff --git a/legacy_config.go b/parsecli/legacy_config.go similarity index 78% rename from legacy_config.go rename to parsecli/legacy_config.go index 4f593f5..e63a727 100644 --- a/legacy_config.go +++ b/parsecli/legacy_config.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "encoding/json" @@ -9,16 +9,16 @@ import ( "github.com/facebookgo/stackerr" ) -const configDir = "config" +const ConfigDir = "config" -var legacyConfigFile = filepath.Join(configDir, "global.json") +var LegacyConfigFile = filepath.Join(ConfigDir, "global.json") type legacyConfig struct { Global struct { ParseVersion string `json:"parseVersion,omitempty"` ParserEmail string `json:"email,omitempty"` } `json:"global,omitempty"` - Applications map[string]*parseAppConfig `json:"applications,omitempty"` + Applications map[string]*ParseAppConfig `json:"applications,omitempty"` } func readLegacyConfigFile(path string) (*legacyConfig, error) { @@ -47,8 +47,8 @@ func writeLegacyConfigFile(c *legacyConfig, path string) error { return stackerr.Wrap(ioutil.WriteFile(path, b, 0600)) } -func getLegacyProjectRoot(e *env, cur string) string { - if _, err := os.Stat(filepath.Join(cur, legacyConfigFile)); err == nil { +func GetLegacyProjectRoot(e *Env, cur string) string { + if _, err := os.Stat(filepath.Join(cur, LegacyConfigFile)); err == nil { return cur } @@ -59,7 +59,7 @@ func getLegacyProjectRoot(e *env, cur string) string { base = filepath.Base(root) root = filepath.Dir(root) if base == "cloud" || base == "public" || base == "config" { - if _, err := os.Stat(filepath.Join(root, legacyConfigFile)); err == nil { + if _, err := os.Stat(filepath.Join(root, LegacyConfigFile)); err == nil { return root } } diff --git a/legacy_config_test.go b/parsecli/legacy_config_test.go similarity index 59% rename from legacy_config_test.go rename to parsecli/legacy_config_test.go index 869e94b..8f6823e 100644 --- a/legacy_config_test.go +++ b/parsecli/legacy_config_test.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "io/ioutil" @@ -9,10 +9,10 @@ import ( "github.com/facebookgo/ensure" ) -var defaultParseConfig = &parseConfig{ - projectConfig: &projectConfig{ - Type: legacyParseFormat, - Parse: &parseProjectConfig{}, +var defaultParseConfig = &ParseConfig{ + ProjectConfig: &ProjectConfig{ + Type: LegacyParseFormat, + Parse: &ParseProjectConfig{}, }, } @@ -20,9 +20,9 @@ func TestConfigAddAlias(t *testing.T) { t.Parallel() const name = "foo" const link = "bar" - c := parseConfig{Applications: map[string]*parseAppConfig{link: {}}} - ensure.Nil(t, c.addAlias(name, link)) - ensure.DeepEqual(t, c.Applications, map[string]*parseAppConfig{ + c := ParseConfig{Applications: map[string]*ParseAppConfig{link: {}}} + ensure.Nil(t, c.AddAlias(name, link)) + ensure.DeepEqual(t, c.Applications, map[string]*ParseAppConfig{ link: {}, name: {Link: link}, }) @@ -32,52 +32,52 @@ func TestConfigAddAliasBadLink(t *testing.T) { t.Parallel() const name = "foo" const link = "bar" - c := parseConfig{} - ensure.Err(t, c.addAlias(name, link), regexp.MustCompile("wasn't found")) + c := ParseConfig{} + ensure.Err(t, c.AddAlias(name, link), regexp.MustCompile("wasn't found")) } func TestConfigAddAliasDupe(t *testing.T) { t.Parallel() const name = "foo" const link = "bar" - c := parseConfig{Applications: map[string]*parseAppConfig{link: {}}} - ensure.Nil(t, c.addAlias(name, link)) - ensure.Err(t, c.addAlias(name, link), regexp.MustCompile("has already been added")) + c := ParseConfig{Applications: map[string]*ParseAppConfig{link: {}}} + ensure.Nil(t, c.AddAlias(name, link)) + ensure.Err(t, c.AddAlias(name, link), regexp.MustCompile("has already been added")) } func TestConfigSetDefault(t *testing.T) { t.Parallel() const name = "foo" - c := parseConfig{Applications: map[string]*parseAppConfig{name: {}}} - ensure.Nil(t, c.setDefaultApp(name)) - ensure.DeepEqual(t, c.Applications, map[string]*parseAppConfig{ + c := ParseConfig{Applications: map[string]*ParseAppConfig{name: {}}} + ensure.Nil(t, c.SetDefaultApp(name)) + ensure.DeepEqual(t, c.Applications, map[string]*ParseAppConfig{ name: {}, - defaultKey: {Link: name}, + DefaultKey: {Link: name}, }) } func TestConfigApp(t *testing.T) { t.Parallel() const name = "foo" - c := parseConfig{Applications: map[string]*parseAppConfig{name: {}}} - ac, err := c.app(name) + c := ParseConfig{Applications: map[string]*ParseAppConfig{name: {}}} + ac, err := c.App(name) ensure.Nil(t, err) - ensure.DeepEqual(t, ac, &parseAppConfig{}) + ensure.DeepEqual(t, ac, &ParseAppConfig{}) } func TestConfigAppNotFound(t *testing.T) { t.Parallel() const name = "foo" - c := parseConfig{Applications: map[string]*parseAppConfig{}} - ac, err := c.app(name) + c := ParseConfig{Applications: map[string]*ParseAppConfig{}} + ac, err := c.App(name) ensure.True(t, ac == nil) ensure.Err(t, err, regexp.MustCompile(`App "foo" wasn't found`)) } func TestConfigAppDefaultNotFound(t *testing.T) { t.Parallel() - c := parseConfig{Applications: map[string]*parseAppConfig{}} - ac, err := c.app(defaultKey) + c := ParseConfig{Applications: map[string]*ParseAppConfig{}} + ac, err := c.App(DefaultKey) ensure.True(t, ac == nil) ensure.Err(t, err, regexp.MustCompile("No default app configured")) } @@ -86,12 +86,12 @@ func TestConfigAppLink(t *testing.T) { t.Parallel() const name = "foo" const link = "bar" - expected := &parseAppConfig{ApplicationID: "xyz"} - c := parseConfig{Applications: map[string]*parseAppConfig{ + expected := &ParseAppConfig{ApplicationID: "xyz"} + c := ParseConfig{Applications: map[string]*ParseAppConfig{ link: expected, name: {Link: link}, }} - ac, err := c.app(name) + ac, err := c.App(name) ensure.Nil(t, err) ensure.DeepEqual(t, ac, expected) } @@ -101,7 +101,7 @@ func TestConfigMissing(t *testing.T) { dir, err := ioutil.TempDir("", "parse-cli-config-") ensure.Nil(t, err) defer os.RemoveAll(dir) - c, err := configFromDir(dir) + c, err := ConfigFromDir(dir) ensure.True(t, c == nil) ensure.Err(t, err, regexp.MustCompile("Command must be run inside a Parse project.")) } @@ -110,7 +110,7 @@ func TestConfigGlobalMalformed(t *testing.T) { t.Parallel() dir := makeDirWithConfig(t, "foo") defer os.RemoveAll(dir) - c, err := configFromDir(dir) + c, err := ConfigFromDir(dir) ensure.True(t, c == nil) ensure.Err(t, err, regexp.MustCompile("is not valid JSON")) } @@ -119,7 +119,7 @@ func TestConfigGlobalEmpty(t *testing.T) { t.Parallel() dir := makeDirWithConfig(t, "") defer os.RemoveAll(dir) - c, err := configFromDir(dir) + c, err := ConfigFromDir(dir) ensure.True(t, c == nil) ensure.Err(t, err, regexp.MustCompile("is not valid JSON")) } @@ -128,7 +128,7 @@ func TestConfigEmptyGlobalOnly(t *testing.T) { t.Parallel() dir := makeDirWithConfig(t, "{}") defer os.RemoveAll(dir) - c, err := configFromDir(dir) + c, err := ConfigFromDir(dir) ensure.Nil(t, err) - ensure.DeepEqual(t, c.getNumApps(), 0) + ensure.DeepEqual(t, c.GetNumApps(), 0) } diff --git a/login.go b/parsecli/login.go similarity index 77% rename from login.go rename to parsecli/login.go index bc2cc52..3453a2d 100644 --- a/login.go +++ b/parsecli/login.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "errors" @@ -22,15 +22,15 @@ import ( const keysURL = "https://www.parse.com/account/keys" -type credentials struct { - email string - password string - token string +type Credentials struct { + Email string + Password string + Token string } -type login struct { - credentials credentials - tokenReader io.Reader +type Login struct { + Credentials Credentials + TokenReader io.Reader } var ( @@ -50,13 +50,13 @@ func accessKeyNotFound(err error) bool { return keyNotFound.MatchString(err.Error()) } -func (l *login) populateCreds(e *env) error { - if l.credentials.email != "" && l.credentials.password != "" { +func (l *Login) populateCreds(e *Env) error { + if l.Credentials.Email != "" && l.Credentials.Password != "" { return nil } fmt.Fprint(e.Out, "Email: ") - fmt.Fscanf(e.In, "%s\n", &l.credentials.email) + fmt.Fscanf(e.In, "%s\n", &l.Credentials.Email) var ( password string @@ -73,14 +73,14 @@ func (l *login) populateCreds(e *env) error { } if password != "" { - l.credentials.password = password + l.Credentials.Password = password } return nil } -func (l *login) getTokensReader() (io.Reader, error) { - if l.tokenReader != nil { - return l.tokenReader, nil +func (l *Login) getTokensReader() (io.Reader, error) { + if l.TokenReader != nil { + return l.TokenReader, nil } homeDir, err := homedir.Dir() if err != nil { @@ -91,11 +91,11 @@ func (l *login) getTokensReader() (io.Reader, error) { if err != nil { return nil, stackerr.Wrap(err) } - l.tokenReader = file + l.TokenReader = file return file, nil } -func (l *login) getTokenCredentials(e *env, email string) (bool, *credentials, error) { +func (l *Login) GetTokenCredentials(e *Env, email string) (bool, *Credentials, error) { reader, err := l.getTokensReader() if err != nil { return false, nil, stackerr.Wrap(err) @@ -111,8 +111,8 @@ func (l *login) getTokenCredentials(e *env, email string) (bool, *credentials, e machine := tokens.FindMachine(server) if machine != nil { return true, - &credentials{ - token: machine.Password, + &Credentials{ + Token: machine.Password, }, nil } @@ -129,8 +129,8 @@ func (l *login) getTokenCredentials(e *env, email string) (bool, *credentials, e machine = tokens.FindMachine(server) if machine != nil { return false, - &credentials{ - token: machine.Password, + &Credentials{ + Token: machine.Password, }, nil } return false, @@ -144,11 +144,11 @@ and default access key not configured for %q ) } -func (l *login) updatedNetrcContent( - e *env, +func (l *Login) updatedNetrcContent( + e *Env, content io.Reader, email string, - credentials *credentials, + credentials *Credentials, ) ([]byte, error) { tokens, err := netrc.Parse(content) if err != nil { @@ -161,9 +161,9 @@ func (l *login) updatedNetrcContent( } machine := tokens.FindMachine(server) if machine == nil { - machine = tokens.NewMachine(server, "default", credentials.token, "") + machine = tokens.NewMachine(server, "default", credentials.Token, "") } else { - machine.UpdatePassword(credentials.token) + machine.UpdatePassword(credentials.Token) } updatedContent, err := tokens.MarshalText() @@ -173,8 +173,8 @@ func (l *login) updatedNetrcContent( return updatedContent, nil } -func (l *login) storeCredentials(e *env, email string, credentials *credentials) error { - if l.tokenReader != nil { +func (l *Login) StoreCredentials(e *Env, email string, credentials *Credentials) error { + if l.TokenReader != nil { // tests should not store credentials return nil } @@ -202,7 +202,7 @@ func (l *login) storeCredentials(e *env, email string, credentials *credentials) return stackerr.Wrap(err) } -func (l *login) authToken(e *env, token string) (string, error) { +func (l *Login) AuthToken(e *Env, token string) (string, error) { req := &http.Request{ Method: "POST", URL: &url.URL{Path: "accountkey"}, @@ -220,37 +220,37 @@ func (l *login) authToken(e *env, token string) (string, error) { }{} if response, err := e.ParseAPIClient.Do(req, nil, res); err != nil { if response.StatusCode == http.StatusUnauthorized { - return "", stackerr.Newf(tokenErrMsgf, last4(token), keysURL) + return "", stackerr.Newf(tokenErrMsgf, Last4(token), keysURL) } return "", stackerr.Wrap(err) } if e.ParserEmail != "" && res.Email != e.ParserEmail { - return "", stackerr.Newf("Account key %q does not belong to %q", last4(token), e.ParserEmail) + return "", stackerr.Newf("Account key %q does not belong to %q", Last4(token), e.ParserEmail) } return res.Email, nil } -func (l *login) authUserWithToken(e *env) (string, error) { - _, tokenCredentials, err := l.getTokenCredentials(e, e.ParserEmail) +func (l *Login) authUserWithToken(e *Env) (string, error) { + _, tokenCredentials, err := l.GetTokenCredentials(e, e.ParserEmail) if err != nil { if stackerr.HasUnderlying(err, stackerr.MatcherFunc(accessKeyNotFound)) { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) } return "", err } - email, err := l.authToken(e, tokenCredentials.token) + email, err := l.AuthToken(e, tokenCredentials.Token) if err != nil { - fmt.Fprintf(e.Err, "Account key could not be used.\nError: %s\n\n", errorString(e, err)) + fmt.Fprintf(e.Err, "Account key could not be used.\nError: %s\n\n", ErrorString(e, err)) return "", err } - l.credentials = *tokenCredentials + l.Credentials = *tokenCredentials return email, nil } -func (l *login) authUser(e *env) error { +func (l *Login) AuthUser(e *Env) error { _, err := l.authUserWithToken(e) if err == nil { return nil @@ -265,7 +265,7 @@ To save time logging in, you should create an account key. `) } - apps := &apps{} + apps := &Apps{} fmt.Fprintln( e.Out, `Type "parse configure accountkey" to create a new account key. @@ -278,8 +278,8 @@ Please login to Parse using your email and password.`, if err != nil { return err } - apps.login.credentials = l.credentials - _, err = apps.restFetchApps(e) + apps.Login.Credentials = l.Credentials + _, err = apps.RestFetchApps(e) if err == nil { return nil } @@ -288,15 +288,15 @@ Please login to Parse using your email and password.`, return err } if err != errAuth { - fmt.Fprintf(e.Err, "Got error: %s", errorString(e, err)) + fmt.Fprintf(e.Err, "Got error: %s", ErrorString(e, err)) } fmt.Fprintf(e.Err, "%s\nPlease try again...\n", err) - l.credentials.password = "" + l.Credentials.Password = "" } return errAuth } -func (l *login) helpCreateToken(e *env) (string, error) { +func (l *Login) HelpCreateToken(e *Env) (string, error) { for i := 0; i < 4; i++ { fmt.Fprintf(e.Out, ` Input your account key or press ENTER to generate a new one. diff --git a/parsecli/login_test.go b/parsecli/login_test.go new file mode 100644 index 0000000..2d44d25 --- /dev/null +++ b/parsecli/login_test.go @@ -0,0 +1,211 @@ +package parsecli + +import ( + "regexp" + "strings" + "testing" + + "github.com/facebookgo/ensure" +) + +func TestPopulateCreds(t *testing.T) { + t.Parallel() + + h := NewHarness(t) + defer h.Stop() + + l := &Login{} + h.Env.In = strings.NewReader("email\npassword\n") + ensure.Nil(t, l.populateCreds(h.Env)) + ensure.DeepEqual(t, l.Credentials.Email, "email") + ensure.DeepEqual(t, l.Credentials.Password, "password") +} + +func TestGetTokenCredentials(t *testing.T) { + t.Parallel() + + h := NewHarness(t) + defer h.Stop() + + l := &Login{} + h.Env.Server = "http://api.example.com/1/" + + l.TokenReader = strings.NewReader( + `machine api.example.com + login default + password token + `, + ) + _, credentials, err := l.GetTokenCredentials(h.Env, "") + ensure.Nil(t, err) + ensure.DeepEqual(t, credentials.Token, "token") + + l.TokenReader = strings.NewReader( + `machine api.example.com + login default + password token + `, + ) + h.Env.Server = "http://api.parse.com" + _, credentials, err = l.GetTokenCredentials(h.Env, "") + ensure.Err(t, err, keyNotFound) + + l = &Login{} + h.Env.Server = "http://api.example.com/1/" + + l.TokenReader = strings.NewReader( + `machine api.example.com#email + login default + password token + `, + ) + _, credentials, err = l.GetTokenCredentials(h.Env, "email") + ensure.Nil(t, err) + ensure.DeepEqual(t, credentials.Token, "token") + + l.TokenReader = strings.NewReader( + `machine api.example.com#email + login default + password token + `, + ) + h.Env.Server = "http://api.parse.com" + _, credentials, err = l.GetTokenCredentials(h.Env, "email") + ensure.Err(t, err, keyNotFound) + + l = &Login{} + h.Env.Server = "http://api.example.com/1/" + + l.TokenReader = strings.NewReader( + `machine api.example.com#email + login default + password token1 +machine api.example.com + login default + password token2 +`, + ) + _, credentials, err = l.GetTokenCredentials(h.Env, "email") + ensure.Nil(t, err) + ensure.DeepEqual(t, credentials.Token, "token1") + + l.TokenReader = strings.NewReader( + `machine api.example.com#email + login default + password token1 +machine api.example.com + login default + password token2 +`, + ) + + _, credentials, err = l.GetTokenCredentials(h.Env, "xmail") + ensure.Nil(t, err) + ensure.DeepEqual(t, credentials.Token, "token2") +} + +func TestAuthUserWithToken(t *testing.T) { + t.Parallel() + + h := NewTokenHarness(t) + defer h.Stop() + + l := &Login{} + h.Env.ParserEmail = "email" + h.Env.Server = "http://api.example.org/1/" + + l.TokenReader = strings.NewReader( + `machine api.example.org#email + login default + password token + `, + ) + _, err := l.authUserWithToken(h.Env) + ensure.Nil(t, err) + + h.Env.ParserEmail = "email2" + + l.TokenReader = strings.NewReader( + `machine api.example.org#email2 + login default + password token + `, + ) + _, err = l.authUserWithToken(h.Env) + ensure.Err(t, err, regexp.MustCompile(`does not belong to "email2"`)) + + h.Env.ParserEmail = "" + + l.TokenReader = strings.NewReader( + `machine api.example.org + login default + password token2 + `, + ) + _, err = l.authUserWithToken(h.Env) + ensure.Err(t, err, regexp.MustCompile("provided is not valid")) + +} + +func TestUpdatedNetrcContent(t *testing.T) { + t.Parallel() + + h := NewHarness(t) + defer h.Stop() + + l := &Login{} + + h.Env.Server = "https://api.example.com/1/" + updated, err := l.updatedNetrcContent( + h.Env, + strings.NewReader( + `machine api.example.com#email + login default + password token0 + +machine api.example.org + login default + password token +`, + ), + "email", + &Credentials{Token: "token"}, + ) + + ensure.Nil(t, err) + ensure.DeepEqual(t, + string(updated), + `machine api.example.com#email + login default + password token + +machine api.example.org + login default + password token +`, + ) + + h.Env.Server = "https://api.example.org/1/" + updated, err = l.updatedNetrcContent(h.Env, + strings.NewReader( + `machine api.example.com + login default + password token +`, + ), + "email", + &Credentials{Token: "token"}, + ) + + ensure.Nil(t, err) + ensure.DeepEqual(t, + string(updated), + `machine api.example.com + login default + password token + +machine api.example.org#email + login default + password token`, + ) +} diff --git a/parsecli/main.go b/parsecli/main.go new file mode 100644 index 0000000..5bb50c2 --- /dev/null +++ b/parsecli/main.go @@ -0,0 +1,226 @@ +package parsecli + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/facebookgo/clock" + "github.com/facebookgo/ensure" + "github.com/facebookgo/parse" + "github.com/facebookgo/testname" +) + +const ( + Version = "2.2.8" + CloudDir = "cloud" + HostingDir = "public" + DefaultBaseURL = "https://api.parse.com/1/" +) + +var UserAgent = fmt.Sprintf("parse-cli-%s-%s", runtime.GOOS, Version) + +type Env struct { + Root string // project root + Server string // parse api server + Type int // project type + ParserEmail string // email associated with developer parse account + ErrorStack bool + Out io.Writer + Err io.Writer + In io.Reader + Exit func(int) + Clock clock.Clock + ParseAPIClient *ParseAPIClient +} + +type Harness struct { + T testing.TB + Out bytes.Buffer + Err bytes.Buffer + Clock *clock.Mock + Env *Env + remove []string +} + +func (h *Harness) MakeEmptyRoot() { + var err error + prefix := fmt.Sprintf("%s-", testname.Get("parse-cli-")) + h.Env.Root, err = ioutil.TempDir("", prefix) + ensure.Nil(h.T, err) + h.remove = append(h.remove, h.Env.Root) +} + +func (h *Harness) MakeWithConfig(global string) { + h.Env.Root = makeDirWithConfig(h.T, global) +} + +func (h *Harness) Stop() { + for _, p := range h.remove { + os.RemoveAll(p) + } +} + +func NewHarness(t testing.TB) *Harness { + te := Harness{ + T: t, + Clock: clock.NewMock(), + } + te.Env = &Env{ + Out: &te.Out, + Err: &te.Err, + Clock: te.Clock, + ParseAPIClient: &ParseAPIClient{APIClient: &parse.Client{}}, + } + return &te +} + +// makes a temp directory with the given global config. +func makeDirWithConfig(t testing.TB, global string) string { + dir, err := ioutil.TempDir("", testname.Get("parse-cli-")) + ensure.Nil(t, err) + ensure.Nil(t, os.Mkdir(filepath.Join(dir, "config"), 0755)) + ensure.Nil(t, ioutil.WriteFile( + filepath.Join(dir, LegacyConfigFile), + []byte(global), + os.FileMode(0600), + )) + return dir +} + +type TransportFunc func(r *http.Request) (*http.Response, error) + +func (t TransportFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return t(r) +} + +func NewTokenHarness(t testing.TB) *Harness { + h := NewHarness(t) + ht := TransportFunc(func(r *http.Request) (*http.Response, error) { + ensure.DeepEqual(t, r.URL.Path, "/1/accountkey") + ensure.DeepEqual(t, r.Method, "POST") + + key := &struct { + AccountKey string `json:"accountKey"` + }{} + ensure.Nil(t, json.NewDecoder(ioutil.NopCloser(r.Body)).Decode(key)) + + if key.AccountKey != "token" { + return &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect token"}`)), + }, nil + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(`{"email": "email"}`)), + }, nil + }) + h.Env.ParseAPIClient = &ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + return h +} + +func NewAppHarness(t testing.TB) (*Harness, []*App) { + h := NewHarness(t) + + apps := []*App{ + newTestApp("A"), + newTestApp("B"), + } + res := map[string][]*App{"results": apps} + ht := TransportFunc(func(r *http.Request) (*http.Response, error) { + + email := r.Header.Get("X-Parse-Email") + password := r.Header.Get("X-Parse-Password") + token := r.Header.Get("X-Parse-Account-Key") + if !((email == "email" && password == "password") || (token == "token")) { + return &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect credentials"}`)), + }, nil + } + + switch r.URL.Path { + case "/1/apps": + if r.Method == "GET" { + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), + }, nil + + } + + if r.Method != "POST" || r.Body == nil { + return &http.Response{ + StatusCode: http.StatusNotFound, + }, errors.New("unknown resource") + } + + var params map[string]string + if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + }, err + } + details, err := json.Marshal(newTestApp(params["appName"])) + if err != nil { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + }, err + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(details)), + }, nil + + case "/1/apps/an-app": + ensure.DeepEqual(t, r.Method, "GET") + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, newTestApp("an-app")))), + }, nil + default: + return &http.Response{ + StatusCode: http.StatusNotFound, + }, nil + } + }) + + h.Env.ParseAPIClient = &ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + return h, apps +} + +func newTestApp(suffix string) *App { + return &App{ + Name: suffix, + DashboardURL: fmt.Sprintf("https://api.example.com/dashboard/%s", suffix), + ApplicationID: fmt.Sprintf("applicationID.%s", suffix), + ClientKey: fmt.Sprintf("clientKey.%s", suffix), + JavaScriptKey: fmt.Sprintf("javaScriptKey.%s", suffix), + WindowsKey: fmt.Sprintf("windowsKey.%s", suffix), + WebhookKey: fmt.Sprintf("webhookKey.%s", suffix), + RestKey: fmt.Sprintf("restKey.%s", suffix), + MasterKey: fmt.Sprintf("masterKey.%s", suffix), + ClientPushEnabled: false, + ClientClassCreationEnabled: true, + RequireRevocableSessions: true, + RevokeSessionOnPasswordChange: true, + } +} + +func jsonStr(t testing.TB, v interface{}) string { + b, err := json.Marshal(v) + ensure.Nil(t, err) + return string(b) +} diff --git a/parse_config.go b/parsecli/parse_config.go similarity index 54% rename from parse_config.go rename to parsecli/parse_config.go index 5abacb7..e37ce9e 100644 --- a/parse_config.go +++ b/parsecli/parse_config.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "encoding/json" @@ -9,11 +9,11 @@ import ( "github.com/facebookgo/stackerr" ) -type parseProjectConfig struct { +type ParseProjectConfig struct { JSSDK string `json:"jssdk,omitempty"` } -type parseAppConfig struct { +type ParseAppConfig struct { ApplicationID string `json:"applicationId,omitempty"` MasterKey string `json:"masterKey,omitempty"` Link string `json:"link,omitempty"` @@ -21,18 +21,23 @@ type parseAppConfig struct { masterKey string } -func (c *parseAppConfig) getApplicationID() string { +func (c *ParseAppConfig) WithInternalMasterKey(masterKey string) *ParseAppConfig { + c.masterKey = masterKey + return c +} + +func (c *ParseAppConfig) GetApplicationID() string { return c.ApplicationID } -func (c *parseAppConfig) getMasterKey(e *env) (string, error) { +func (c *ParseAppConfig) GetMasterKey(e *Env) (string, error) { if c.MasterKey != "" { return c.MasterKey, nil } if c.masterKey != "" { return c.masterKey, nil } - app, err := fetchAppKeys(e, c.getApplicationID()) + app, err := fetchAppKeys(e, c.GetApplicationID()) if err != nil { return "", err } @@ -40,69 +45,69 @@ func (c *parseAppConfig) getMasterKey(e *env) (string, error) { return app.MasterKey, nil } -func (c *parseAppConfig) getApplicationAuth(e *env) (string, error) { - return c.getMasterKey(e) +func (c *ParseAppConfig) GetApplicationAuth(e *Env) (string, error) { + return c.GetMasterKey(e) } -func (c *parseAppConfig) getLink() string { +func (c *ParseAppConfig) GetLink() string { return c.Link } -type parseConfig struct { - Applications map[string]*parseAppConfig `json:"applications,omitempty"` - projectConfig *projectConfig +type ParseConfig struct { + Applications map[string]*ParseAppConfig `json:"applications,omitempty"` + ProjectConfig *ProjectConfig `json:"-"` } -func (c *parseConfig) addAlias(name, link string) error { +func (c *ParseConfig) AddAlias(name, link string) error { if _, found := c.Applications[name]; found { return stackerr.Newf("App %q has already been added.", name) } if _, found := c.Applications[link]; !found { return stackerr.Newf("App %q wasn't found.", link) } - c.Applications[name] = &parseAppConfig{Link: link} + c.Applications[name] = &ParseAppConfig{Link: link} return nil } -func (c *parseConfig) setDefaultApp(name string) error { - delete(c.Applications, defaultKey) - return c.addAlias(defaultKey, name) +func (c *ParseConfig) SetDefaultApp(name string) error { + delete(c.Applications, DefaultKey) + return c.AddAlias(DefaultKey, name) } -func (c *parseConfig) app(name string) (appConfig, error) { +func (c *ParseConfig) App(name string) (AppConfig, error) { ac, found := c.Applications[name] if !found { - if name == defaultKey { + if name == DefaultKey { return nil, stackerr.Newf("No default app configured.") } return nil, stackerr.Newf("App %q wasn't found.", name) } if ac.Link != "" { - return c.app(ac.Link) + return c.App(ac.Link) } return ac, nil } -func (c *parseConfig) getProjectConfig() *projectConfig { - return c.projectConfig +func (c *ParseConfig) GetProjectConfig() *ProjectConfig { + return c.ProjectConfig } -func (c *parseConfig) getDefaultApp() string { +func (c *ParseConfig) GetDefaultApp() string { var defaultApp string - if defaultKeyLink, ok := c.Applications[defaultKey]; ok { - defaultApp = defaultKeyLink.Link + if DefaultKeyLink, ok := c.Applications[DefaultKey]; ok { + defaultApp = DefaultKeyLink.Link } return defaultApp } -func (c *parseConfig) getNumApps() int { +func (c *ParseConfig) GetNumApps() int { return len(c.Applications) } -func (c *parseConfig) pprintApps(e *env) { +func (c *ParseConfig) PrettyPrintApps(e *Env) { apps := c.Applications - defaultApp := c.getDefaultApp() + defaultApp := c.GetDefaultApp() var appNames []string for appName := range apps { @@ -120,7 +125,7 @@ func (c *parseConfig) pprintApps(e *env) { ) for _, appName := range appNames { - if appName == defaultKey { + if appName == DefaultKey { continue } if defaultApp == appName { @@ -130,39 +135,39 @@ func (c *parseConfig) pprintApps(e *env) { } fmt.Fprintf(e.Out, "%s", appName) - if config, _ := apps[appName]; config.getLink() != "" { - fmt.Fprintf(e.Out, " -> %s", config.getLink()) + if config, _ := apps[appName]; config.GetLink() != "" { + fmt.Fprintf(e.Out, " -> %s", config.GetLink()) } fmt.Fprintln(e.Out, "") } } -func readParseConfigFile(path string) (*parseConfig, error) { +func readParseConfigFile(path string) (*ParseConfig, error) { f, err := os.Open(path) if err != nil { return nil, stackerr.Wrap(err) } defer f.Close() - var c parseConfig + var c ParseConfig if err := json.NewDecoder(f).Decode(&c); err != nil { return nil, stackerr.Newf("Config file %q is not valid JSON.", path) } return &c, nil } -func setParseDefault(e *env, newDefault, defaultApp string, parseConfig *parseConfig) error { +func setParseDefault(e *Env, newDefault, defaultApp string, parseConfig *ParseConfig) error { apps := parseConfig.Applications if _, ok := apps[newDefault]; !ok { - parseConfig.pprintApps(e) + parseConfig.PrettyPrintApps(e) return stackerr.Newf("Invalid application name \"%s\". Please select from the valid applications printed above.", newDefault) } if defaultApp == "" { - apps[defaultKey] = &parseAppConfig{Link: newDefault} + apps[DefaultKey] = &ParseAppConfig{Link: newDefault} } else { - apps[defaultKey].Link = newDefault + apps[DefaultKey].Link = newDefault } - if err := storeConfig(e, parseConfig); err != nil { + if err := StoreConfig(e, parseConfig); err != nil { return err } fmt.Fprintf(e.Out, "Default app set to %s.\n", newDefault) diff --git a/run_helpers.go b/parsecli/runners.go similarity index 62% rename from run_helpers.go rename to parsecli/runners.go index 4f3ed5c..a644caa 100644 --- a/run_helpers.go +++ b/parsecli/runners.go @@ -1,4 +1,4 @@ -package main +package parsecli import ( "fmt" @@ -8,8 +8,8 @@ import ( type cobraRun func(cmd *cobra.Command, args []string) -// runNoArgs wraps a run function that shouldn't get any arguments. -func runNoArgs(e *env, f func(*env) error) cobraRun { +// RunNoArgs wraps a run function that shouldn't get any arguments. +func RunNoArgs(e *Env, f func(*Env) error) cobraRun { return func(cmd *cobra.Command, args []string) { if len(args) != 0 { fmt.Fprintf(e.Err, "unexpected arguments:%+v\n\n", args) @@ -17,27 +17,27 @@ func runNoArgs(e *env, f func(*env) error) cobraRun { e.Exit(1) } if err := f(e); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } } } -// runWithArgs wraps a run function that can access arguments to cobraCmd -func runWithArgs(e *env, f func(*env, []string) error) cobraRun { +// RunWithArgs wraps a run function that can access arguments to cobraCmd +func RunWithArgs(e *Env, f func(*Env, []string) error) cobraRun { return func(cmd *cobra.Command, args []string) { if err := f(e, args); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } } } -// runWithClient wraps a run function that should get an app, when the default is +// RunWithClient wraps a run function that should get an app, when the default is // picked from the config in the current working directory. -func runWithClient(e *env, f func(*env, *context) error) cobraRun { +func RunWithClient(e *Env, f func(*Env, *Context) error) cobraRun { return func(cmd *cobra.Command, args []string) { - app := defaultKey + app := DefaultKey if len(args) > 1 { fmt.Fprintf( e.Err, @@ -52,18 +52,18 @@ func runWithClient(e *env, f func(*env, *context) error) cobraRun { } cl, err := newContext(e, app) if err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } if err := f(e, cl); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } } } -// runWithAppClient wraps a run function that should get an app -func runWithAppClient(e *env, f func(*env, *context) error) cobraRun { +// RunWithAppClient wraps a run function that should get an app +func RunWithAppClient(e *Env, f func(*Env, *Context) error) cobraRun { return func(cmd *cobra.Command, args []string) { if len(args) == 0 { fmt.Fprintf(e.Err, "please provide an app name\n\n") @@ -82,33 +82,33 @@ func runWithAppClient(e *env, f func(*env, *context) error) cobraRun { app := args[0] cl, err := newContext(e, app) if err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } if err := f(e, cl); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } } } -// runWithArgsClient wraps a run function that should get an app, whee the default is +// RunWithArgsClient wraps a run function that should get an app, whee the default is // picked from the config in the current working directory. It also passes args to the // runner function -func runWithArgsClient(e *env, f func(*env, *context, []string) error) cobraRun { +func RunWithArgsClient(e *Env, f func(*Env, *Context, []string) error) cobraRun { return func(cmd *cobra.Command, args []string) { - app := defaultKey + app := DefaultKey if len(args) > 1 { app = args[0] args = args[1:] } cl, err := newContext(e, app) if err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } if err := f(e, cl, args); err != nil { - fmt.Fprintln(e.Err, errorString(e, err)) + fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } } diff --git a/parsecli/runners_test.go b/parsecli/runners_test.go new file mode 100644 index 0000000..3127d4d --- /dev/null +++ b/parsecli/runners_test.go @@ -0,0 +1,141 @@ +package parsecli + +import ( + "errors" + "io/ioutil" + "testing" + + "github.com/facebookgo/ensure" + "github.com/spf13/cobra" +) + +func noOpCmd() *cobra.Command { + var c cobra.Command + c.SetOutput(ioutil.Discard) + return &c +} + +func TestRunNoArgsFuncError(t *testing.T) { + t.Parallel() + const message = "hello world" + h := NewHarness(t) + defer h.Stop() + h.Env.Exit = func(i int) { panic(exitCode(i)) } + func() { + defer ensure.PanicDeepEqual(t, exitCode(1)) + r := RunNoArgs(h.Env, func(*Env) error { return errors.New(message) }) + r(noOpCmd(), nil) + }() + ensure.StringContains(t, h.Err.String(), message) +} + +func TestRunWithAppMultipleArgs(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + h.Env.Exit = func(i int) { panic(exitCode(i)) } + func() { + defer ensure.PanicDeepEqual(t, exitCode(1)) + r := RunWithClient(h.Env, nil) + r(noOpCmd(), []string{"foo", "bar"}) + }() + ensure.StringContains( + t, + h.Err.String(), + "only an optional app name is expected", + ) +} + +func TestRunWithAppNonProjectDir(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + h.MakeEmptyRoot() + h.Env.Exit = func(i int) { panic(exitCode(i)) } + func() { + defer ensure.PanicDeepEqual(t, exitCode(1)) + r := RunWithClient(h.Env, nil) + r(noOpCmd(), nil) + }() + ensure.StringContains( + t, + h.Err.String(), + "Command must be run inside a Parse project.", + ) +} + +func TestRunWithAppNotFound(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + c := legacyConfig{Applications: map[string]*ParseAppConfig{"a": {}}} + h.MakeWithConfig(jsonStr(t, c)) + h.Env.Exit = func(i int) { panic(exitCode(i)) } + func() { + defer ensure.PanicDeepEqual(t, exitCode(1)) + r := RunWithClient(h.Env, nil) + r(noOpCmd(), []string{"b"}) + }() + ensure.StringContains(t, h.Err.String(), `App "b" wasn't found`) +} + +func TestRunWithAppNamed(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + const appName = "a" + c := &ParseConfig{ + Applications: map[string]*ParseAppConfig{ + appName: {ApplicationID: "id", MasterKey: "token"}, + }, + } + h.MakeWithConfig(jsonStr(t, c)) + r := RunWithClient(h.Env, func(e *Env, c *Context) error { + ensure.NotNil(t, c) + return nil + }) + r(noOpCmd(), []string{"a"}) +} + +func TestRunWithDefault(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + const appName = "a" + c := &ParseConfig{ + Applications: map[string]*ParseAppConfig{ + appName: {ApplicationID: "id", MasterKey: "token"}, + DefaultKey: {Link: appName}, + }, + } + h.MakeWithConfig(jsonStr(t, c)) + r := RunWithClient(h.Env, func(e *Env, c *Context) error { + ensure.NotNil(t, c) + return nil + }) + r(noOpCmd(), []string{"a"}) +} + +func TestRunWithAppError(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + const appName = "a" + c := &ParseConfig{ + Applications: map[string]*ParseAppConfig{ + appName: {ApplicationID: "id", MasterKey: "token"}, + }, + } + h.MakeWithConfig(jsonStr(t, c)) + h.Env.Exit = func(i int) { panic(exitCode(i)) } + const message = "hello world" + func() { + defer ensure.PanicDeepEqual(t, exitCode(1)) + r := RunWithClient(h.Env, func(e *Env, c *Context) error { + ensure.NotNil(t, c) + return errors.New(message) + }) + r(noOpCmd(), []string{"a"}) + }() + ensure.StringContains(t, h.Err.String(), message) +} diff --git a/parsecli/utils.go b/parsecli/utils.go new file mode 100644 index 0000000..f42a1ff --- /dev/null +++ b/parsecli/utils.go @@ -0,0 +1,208 @@ +package parsecli + +import ( + "errors" + "fmt" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/facebookgo/errgroup" + "github.com/facebookgo/parse" + "github.com/facebookgo/stackerr" +) + +const ( + SampleSource = ` +// Use Parse.Cloud.define to define as many cloud functions as you want. +// For example: +Parse.Cloud.define("hello", function(request, response) { + response.success("Hello world!"); +}); +` + + SampleHTML = ` + + + My ParseApp site + + + +
+

Congratulations! You're already hosting with Parse.

+

To get started, edit this file at public/index.html and start adding static content.

+

If you want something a bit more dynamic, delete this file and check out our hosting docs.

+
+ + +` +) + +func getHostFromURL(urlStr, email string) (string, error) { + netURL, err := url.Parse(urlStr) + if err != nil { + return "", stackerr.Wrap(err) + } + server := regexp.MustCompile(`(.*):\d+$`).ReplaceAllString(netURL.Host, "$1") + if server == "" { + return "", stackerr.Newf("%s is not a valid url", urlStr) + } + if email != "" { + return fmt.Sprintf("%s#%s", server, email), nil + } + return server, nil +} + +func Last4(str string) string { + l := len(str) + if l > 4 { + return fmt.Sprintf("%s%s", strings.Repeat("*", l-4), str[l-4:l]) + } + return str +} + +// errorString returns the error string with our without the stack trace +// depending on the Environment variable. this exists because we want plain +// messages for end users, but when we're working on the CLI we want the stack +// trace for debugging. +func ErrorString(e *Env, err error) string { + type hasUnderlying interface { + HasUnderlying() error + } + + parseErr := func(err error) error { + if apiErr, ok := err.(*parse.Error); ok { + return errors.New(apiErr.Message) + } + return err + } + + lastErr := func(err error) error { + if serr, ok := err.(*stackerr.Error); ok { + if errs := stackerr.Underlying(serr); len(errs) != 0 { + err = errs[len(errs)-1] + } + } else { + if eu, ok := err.(hasUnderlying); ok { + err = eu.HasUnderlying() + } + } + + return parseErr(err) + } + + if !e.ErrorStack { + if merr, ok := err.(errgroup.MultiError); ok { + var multiError []error + for _, ierr := range []error(merr) { + multiError = append(multiError, lastErr(ierr)) + } + err = errgroup.MultiError(multiError) + } else { + err = lastErr(err) + } + return parseErr(err).Error() + } + + return err.Error() +} + +func CreateConfigWithContent(path, content string) error { + file, err := os.OpenFile( + path, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, + 0600, + ) + if err != nil && !os.IsExist(err) { + return stackerr.Wrap(err) + } + defer file.Close() + if _, err := file.WriteString(content); err != nil { + return stackerr.Wrap(err) + } + if err := file.Close(); err != nil { + return stackerr.Wrap(err) + } + return nil +} + +var NewProjectFiles = []struct { + Dirname, Filename, Content string +}{ + {"cloud", "main.js", SampleSource}, + {"public", "index.html", SampleHTML}, +} + +func CloneSampleCloudCode(e *Env, dumpTemplate bool) error { + err := os.MkdirAll(e.Root, 0755) + if err != nil { + return stackerr.Wrap(err) + } + + err = CreateConfigWithContent( + filepath.Join(e.Root, ParseProject), + fmt.Sprintf( + `{ + "project_type" : %d, + "parse": {"jssdk":""} +}`, + ParseFormat, + ), + ) + if err != nil { + return err + } + err = CreateConfigWithContent( + filepath.Join(e.Root, ParseLocal), + "{}", + ) + if err != nil { + return err + } + + // no need to set up the template code + if !dumpTemplate { + return nil + } + + for _, info := range NewProjectFiles { + sampleDir := filepath.Join(e.Root, info.Dirname) + if _, err := os.Stat(sampleDir); err != nil { + if !os.IsNotExist(err) { + return stackerr.Wrap(err) + } + if err := os.Mkdir(sampleDir, 0755); err != nil { + return stackerr.Wrap(err) + } + } + + sampleFile := filepath.Join(sampleDir, info.Filename) + if _, err := os.Stat(sampleFile); err != nil { + if os.IsNotExist(err) { + file, err := os.Create(sampleFile) + if err != nil && !os.IsExist(err) { + return stackerr.Wrap(err) + } + + defer file.Close() + + if _, err := file.WriteString(info.Content); err != nil { + return stackerr.Wrap(err) + } + if err := file.Close(); err != nil { + return stackerr.Wrap(err) + } + } + } + } + return nil +} diff --git a/parsecli/utils_test.go b/parsecli/utils_test.go new file mode 100644 index 0000000..7a294dc --- /dev/null +++ b/parsecli/utils_test.go @@ -0,0 +1,141 @@ +package parsecli + +import ( + "regexp" + "testing" + + "github.com/facebookgo/ensure" + "github.com/facebookgo/errgroup" + "github.com/facebookgo/parse" + "github.com/facebookgo/stackerr" +) + +func TestGetHostFromURL(t *testing.T) { + urlStr := "https://api.parse.com/1/" + host, err := getHostFromURL(urlStr, "") + ensure.Nil(t, err) + ensure.DeepEqual(t, host, "api.parse.com") + + urlStr = "https://api.parse.com/1/" + host, err = getHostFromURL(urlStr, "yolo@earth.com") + ensure.Nil(t, err) + ensure.DeepEqual(t, host, "api.parse.com#yolo@earth.com") + + urlStr = "https://api.example.com:8080/1/" + host, err = getHostFromURL(urlStr, "") + ensure.Nil(t, err) + ensure.DeepEqual(t, host, "api.example.com") + + urlStr = "https://api.example.com:8080/1/" + host, err = getHostFromURL(urlStr, "yolo@earth.com") + ensure.Nil(t, err) + ensure.DeepEqual(t, host, "api.example.com#yolo@earth.com") + + urlStr = "api.example.com:8080:90" + host, err = getHostFromURL(urlStr, "") + ensure.Err(t, err, regexp.MustCompile("not a valid url")) +} + +func TestLastFour(t *testing.T) { + t.Parallel() + + ensure.DeepEqual(t, Last4(""), "") + ensure.DeepEqual(t, Last4("a"), "a") + ensure.DeepEqual(t, Last4("ab"), "ab") + ensure.DeepEqual(t, Last4("abc"), "abc") + ensure.DeepEqual(t, Last4("abcd"), "abcd") + ensure.DeepEqual(t, Last4("abcdefg"), "***defg") +} + +func TestErrorStringWithoutStack(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + h.Env.ErrorStack = false + const message = "hello world" + actual := ErrorString(h.Env, stackerr.New(message)) + ensure.StringContains(t, actual, message) + ensure.StringDoesNotContain(t, actual, ".go") +} + +type exitCode int + +func TestErrorStringWithStack(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + h.Env.ErrorStack = true + const message = "hello world" + actual := ErrorString(h.Env, stackerr.New(message)) + ensure.StringContains(t, actual, message) + ensure.StringContains(t, actual, ".go") +} + +func TestErrorString(t *testing.T) { + t.Parallel() + h := NewHarness(t) + defer h.Stop() + apiErr := &parse.Error{Code: -1, Message: "Error\nMessage"} + ensure.DeepEqual(t, `Error +Message`, + ErrorString(h.Env, apiErr), + ) +} + +func TestStackErrorString(t *testing.T) { + t.Parallel() + + h := NewHarness(t) + defer h.Stop() + + err := stackerr.New("error") + + h.Env.ErrorStack = false + errStr := ErrorString(h.Env, err) + + ensure.DeepEqual(t, errStr, "error") + + h.Env.ErrorStack = true + errStr = ErrorString(h.Env, err) + + ensure.StringContains(t, errStr, "error") + ensure.StringContains(t, errStr, ".go") + + err = stackerr.Wrap(&parse.Error{Message: "message", Code: 1}) + h.Env.ErrorStack = false + errStr = ErrorString(h.Env, err) + + ensure.DeepEqual(t, errStr, "message") + + h.Env.ErrorStack = true + errStr = ErrorString(h.Env, err) + + ensure.StringContains(t, errStr, `parse: api error with code=1 and message="message`) + ensure.StringContains(t, errStr, ".go") +} + +func TestMultiErrorString(t *testing.T) { + t.Parallel() + + h := NewHarness(t) + defer h.Stop() + + err := errgroup.MultiError( + []error{ + stackerr.New("error"), + stackerr.Wrap(&parse.Error{Message: "message", Code: 1}), + }, + ) + + h.Env.ErrorStack = false + errStr := ErrorString(h.Env, err) + + ensure.DeepEqual(t, errStr, "multiple errors: error | message") + + h.Env.ErrorStack = true + errStr = ErrorString(h.Env, err) + + ensure.StringContains(t, errStr, "multiple errors") + ensure.StringContains(t, errStr, `parse: api error with code=1 and message="message"`) + ensure.StringContains(t, errStr, ".go") +} diff --git a/Resources/AndroidManifest.xml b/parsecmd/Resources/AndroidManifest.xml similarity index 100% rename from Resources/AndroidManifest.xml rename to parsecmd/Resources/AndroidManifest.xml diff --git a/Resources/Test.xcarchive/Info.plist b/parsecmd/Resources/Test.xcarchive/Info.plist similarity index 100% rename from Resources/Test.xcarchive/Info.plist rename to parsecmd/Resources/Test.xcarchive/Info.plist diff --git a/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Info.plist b/parsecmd/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Info.plist similarity index 100% rename from Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Info.plist rename to parsecmd/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Info.plist diff --git a/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Resources/DWARF/Test b/parsecmd/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Resources/DWARF/Test similarity index 100% rename from Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Resources/DWARF/Test rename to parsecmd/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Resources/DWARF/Test diff --git a/Resources/build_tools/.empty b/parsecmd/Resources/build_tools/.empty similarity index 100% rename from Resources/build_tools/.empty rename to parsecmd/Resources/build_tools/.empty diff --git a/Resources/mapping.txt b/parsecmd/Resources/mapping.txt similarity index 100% rename from Resources/mapping.txt rename to parsecmd/Resources/mapping.txt diff --git a/add_cmd.go b/parsecmd/add.go similarity index 55% rename from add_cmd.go rename to parsecmd/add.go index eb8eb1c..019b700 100644 --- a/add_cmd.go +++ b/parsecmd/add.go @@ -1,29 +1,30 @@ -package main +package parsecmd import ( "fmt" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" ) -func (a *addCmd) getParseAppConfig(app *app) *parseAppConfig { - return &parseAppConfig{ +func (a *addCmd) getParseAppConfig(app *parsecli.App) *parsecli.ParseAppConfig { + pc := &parsecli.ParseAppConfig{ ApplicationID: app.ApplicationID, - masterKey: app.MasterKey, } + return pc.WithInternalMasterKey(app.MasterKey) } func (a *addCmd) addSelectedParseApp( appName string, - appConfig *parseAppConfig, + appConfig *parsecli.ParseAppConfig, args []string, - e *env, + e *parsecli.Env, ) error { - config, err := configFromDir(e.Root) + config, err := parsecli.ConfigFromDir(e.Root) if err != nil { return err } - parseConfig, ok := config.(*parseConfig) + parseConfig, ok := config.(*parsecli.ParseConfig) if !ok { return stackerr.New("Invalid Cloud Code config.") } @@ -39,22 +40,22 @@ func (a *addCmd) addSelectedParseApp( alias := args[0] aliasConfig, ok := parseConfig.Applications[alias] if !ok { - parseConfig.Applications[alias] = &parseAppConfig{Link: appName} + parseConfig.Applications[alias] = &parsecli.ParseAppConfig{Link: appName} } - if ok && aliasConfig.getLink() != "" { + if ok && aliasConfig.GetLink() != "" { fmt.Fprintf(e.Out, "Overwriting alias: %q to point to %q\n", alias, appName) - parseConfig.Applications[alias] = &parseAppConfig{Link: appName} + parseConfig.Applications[alias] = &parsecli.ParseAppConfig{Link: appName} } } if a.MakeDefault { - if _, ok := parseConfig.Applications[defaultKey]; ok { + if _, ok := parseConfig.Applications[parsecli.DefaultKey]; ok { return stackerr.New(`Default key already set. To override default, use command "parse default"`) } - parseConfig.Applications[defaultKey] = &parseAppConfig{Link: appName} + parseConfig.Applications[parsecli.DefaultKey] = &parsecli.ParseAppConfig{Link: appName} } - if err := storeConfig(e, parseConfig); err != nil { + if err := parsecli.StoreConfig(e, parseConfig); err != nil { return err } if a.verbose { diff --git a/add.go b/parsecmd/add_common.go similarity index 69% rename from add.go rename to parsecmd/add_common.go index f82a2e5..13d75ed 100644 --- a/add.go +++ b/parsecmd/add_common.go @@ -1,25 +1,26 @@ -package main +package parsecmd import ( + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) type addCmd struct { MakeDefault bool - apps *apps + apps *parsecli.Apps verbose bool } func (a *addCmd) addSelectedApp( name string, - appConfig appConfig, + appConfig parsecli.AppConfig, args []string, - e *env, + e *parsecli.Env, ) error { switch e.Type { - case legacyParseFormat, parseFormat: - parseAppConfig, ok := appConfig.(*parseAppConfig) + case parsecli.LegacyParseFormat, parsecli.ParseFormat: + parseAppConfig, ok := appConfig.(*parsecli.ParseAppConfig) if !ok { return stackerr.New("invalid parse app config passed.") } @@ -29,8 +30,8 @@ func (a *addCmd) addSelectedApp( return stackerr.Newf("Unknown project type: %d.", e.Type) } -func (a *addCmd) selectApp(e *env, appName string) (*app, error) { - apps, err := a.apps.restFetchApps(e) +func (a *addCmd) selectApp(e *parsecli.Env, appName string) (*parsecli.App, error) { + apps, err := a.apps.RestFetchApps(e) if err != nil { return nil, err } @@ -43,16 +44,16 @@ func (a *addCmd) selectApp(e *env, appName string) (*app, error) { return nil, stackerr.Newf("You are not a collaborator on app: %s", appName) } - app, err := a.apps.selectApp(apps, "Select an App to add to config: ", e) + app, err := a.apps.SelectApp(apps, "Select an App to add to config: ", e) if err != nil { return nil, err } return app, nil } -func (a *addCmd) run(e *env, args []string) error { +func (a *addCmd) run(e *parsecli.Env, args []string) error { - if err := a.apps.login.authUser(e); err != nil { + if err := a.apps.Login.AuthUser(e); err != nil { return err } var appName string @@ -71,10 +72,10 @@ func (a *addCmd) run(e *env, args []string) error { return a.addSelectedApp(app.Name, appConfig, args, e) } -func newAddCmd(e *env) *cobra.Command { +func NewAddCmd(e *parsecli.Env) *cobra.Command { a := &addCmd{ MakeDefault: false, - apps: &apps{}, + apps: &parsecli.Apps{}, verbose: true, } cmd := &cobra.Command{ @@ -82,7 +83,7 @@ func newAddCmd(e *env) *cobra.Command { Short: "Adds a new Parse App to config in current Cloud Code directory", Long: `Adds a new Parse App to config in current Cloud Code directory. If an argument is given, the added application can also be referenced by that name.`, - Run: runWithArgs(e, a.run), + Run: parsecli.RunWithArgs(e, a.run), } cmd.Flags().BoolVarP(&a.MakeDefault, "default", "d", a.MakeDefault, "Make the selected app default") diff --git a/add_cmd_test.go b/parsecmd/add_test.go similarity index 53% rename from add_cmd_test.go rename to parsecmd/add_test.go index 70b9293..bf9a499 100644 --- a/add_cmd_test.go +++ b/parsecmd/add_test.go @@ -1,6 +1,7 @@ -package main +package parsecmd import ( + "encoding/json" "io/ioutil" "net/http" "os" @@ -8,15 +9,29 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" ) -func newAddCmdHarness(t testing.TB) (*Harness, []*app) { - h := newHarness(t) +var ( + defaultCredentials = parsecli.Credentials{Email: "email", Password: "password"} + defaultTokenCredentials = parsecli.Credentials{Email: "email", Token: "token"} + defaultApps = parsecli.Apps{Login: parsecli.Login{Credentials: defaultCredentials}} + defaultAppsWithToken = parsecli.Apps{Login: parsecli.Login{Credentials: defaultTokenCredentials}} +) + +func jsonStr(t testing.TB, v interface{}) string { + b, err := json.Marshal(v) + ensure.Nil(t, err) + return string(b) +} + +func newAddCmdHarness(t testing.TB) (*parsecli.Harness, []*parsecli.App) { + h := parsecli.NewHarness(t) defer h.Stop() - apps := []*app{ + apps := []*parsecli.App{ { Name: "A", ApplicationID: "appId.A", @@ -28,8 +43,8 @@ func newAddCmdHarness(t testing.TB) (*Harness, []*app) { MasterKey: "masterKey.B", }, } - res := map[string][]*app{"results": apps} - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + res := map[string][]*parsecli.App{"results": apps} + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/apps") email := r.Header.Get("X-Parse-Email") @@ -48,17 +63,17 @@ func newAddCmdHarness(t testing.TB) (*Harness, []*app) { }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} return h, apps } -func newAddCmdHarnessWithConfig(t testing.TB) (*Harness, *addCmd, error) { +func newAddCmdHarnessWithConfig(t testing.TB) (*parsecli.Harness, *addCmd, error) { a := addCmd{apps: &defaultApps} h, _ := newAddCmdHarness(t) - h.makeEmptyRoot() + h.MakeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) + ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) return h, &a, nil } @@ -69,11 +84,11 @@ func TestAddCmdGlobalMakeDefault(t *testing.T) { defer h.Stop() a.MakeDefault = true ensure.Nil(t, err) - h.env.Type = parseFormat - h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) - ensure.Nil(t, a.run(h.env, []string{})) + h.Env.Type = parsecli.ParseFormat + h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) + ensure.Nil(t, a.run(h.Env, []string{})) - content, err := ioutil.ReadFile(filepath.Join(h.env.Root, parseLocal)) + content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.ParseLocal)) ensure.Nil(t, err) ensure.DeepEqual(t, string(content), `{ "applications": { @@ -92,11 +107,11 @@ func TestAddCmdGlobalNotDefault(t *testing.T) { h, a, err := newAddCmdHarnessWithConfig(t) defer h.Stop() ensure.Nil(t, err) - h.env.Type = parseFormat - h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) - ensure.Nil(t, a.run(h.env, []string{})) + h.Env.Type = parsecli.ParseFormat + h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) + ensure.Nil(t, a.run(h.Env, []string{})) - content, err := ioutil.ReadFile(filepath.Join(h.env.Root, parseLocal)) + content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.ParseLocal)) ensure.Nil(t, err) ensure.DeepEqual(t, string(content), `{ "applications": { @@ -109,17 +124,17 @@ func TestAddCmdGlobalNotDefault(t *testing.T) { // NOTE: test functionality for legacy config format -func newLegacyAddCmdHarnessWithConfig(t testing.TB) (*Harness, *addCmd, error) { +func newLegacyAddCmdHarnessWithConfig(t testing.TB) (*parsecli.Harness, *addCmd, error) { a := addCmd{apps: &defaultApps} h, _ := newAddCmdHarness(t) - h.makeEmptyRoot() + h.MakeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) - ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) + ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) + ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) ensure.Nil(t, ioutil.WriteFile( - filepath.Join(h.env.Root, legacyConfigFile), + filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), []byte("{}"), 0600, ), @@ -133,11 +148,11 @@ func TestLegacyAddCmdGlobalMakeDefault(t *testing.T) { defer h.Stop() a.MakeDefault = true ensure.Nil(t, err) - h.env.Type = legacyParseFormat - h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) - ensure.Nil(t, a.run(h.env, []string{})) + h.Env.Type = parsecli.LegacyParseFormat + h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) + ensure.Nil(t, a.run(h.Env, []string{})) - content, err := ioutil.ReadFile(filepath.Join(h.env.Root, legacyConfigFile)) + content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.LegacyConfigFile)) ensure.Nil(t, err) ensure.DeepEqual(t, string(content), `{ "global": {}, @@ -157,11 +172,11 @@ func TestLegacyAddCmdGlobalNotDefault(t *testing.T) { h, a, err := newLegacyAddCmdHarnessWithConfig(t) defer h.Stop() ensure.Nil(t, err) - h.env.Type = legacyParseFormat - h.env.In = ioutil.NopCloser(strings.NewReader("1\n")) - ensure.Nil(t, a.run(h.env, []string{})) + h.Env.Type = parsecli.LegacyParseFormat + h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) + ensure.Nil(t, a.run(h.Env, []string{})) - content, err := ioutil.ReadFile(filepath.Join(h.env.Root, legacyConfigFile)) + content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.LegacyConfigFile)) ensure.Nil(t, err) ensure.DeepEqual(t, string(content), `{ "global": {}, diff --git a/android_symbol_uploader.go b/parsecmd/android_symbol_uploader.go similarity index 94% rename from android_symbol_uploader.go rename to parsecmd/android_symbol_uploader.go index 72e6337..f6e2d7e 100644 --- a/android_symbol_uploader.go +++ b/parsecmd/android_symbol_uploader.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "bytes" @@ -10,6 +10,7 @@ import ( "regexp" "strconv" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" ) @@ -130,7 +131,7 @@ func parseVersionFromBytes(bytes []byte) (int, error) { return int(version), nil } -func (a *androidSymbolUploader) getBuildVersion(e *env) (int, error) { +func (a *androidSymbolUploader) getBuildVersion(e *parsecli.Env) (int, error) { var handleError = func(versionCode int, err error) (int, error) { if err != nil { return 0, err @@ -148,7 +149,7 @@ func (a *androidSymbolUploader) getBuildVersion(e *env) (int, error) { return handleError(a.getBuildVersionFromAPK()) } -func (a *androidSymbolUploader) uploadSymbols(e *env) error { +func (a *androidSymbolUploader) uploadSymbols(e *parsecli.Env) error { appBuildVersion, err := a.getBuildVersion(e) if err != nil { return err diff --git a/android_symbol_uploader_test.go b/parsecmd/android_symbol_uploader_test.go similarity index 94% rename from android_symbol_uploader_test.go rename to parsecmd/android_symbol_uploader_test.go index eeaa516..fd880ae 100644 --- a/android_symbol_uploader_test.go +++ b/parsecmd/android_symbol_uploader_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "fmt" @@ -8,6 +8,7 @@ import ( "regexp" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) @@ -106,7 +107,7 @@ func TestAndroidAcceptsPath(t *testing.T) { func TestFindAAPT(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() aapts := findAAPT("Resources") @@ -158,15 +159,15 @@ func TestGetBuildVersion(t *testing.T) { AAPT: filepath.Join("Resources", "build_tools", "aapt"), } - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - manifestVersion, err := a.getBuildVersion(h.env) + manifestVersion, err := a.getBuildVersion(h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, manifestVersion, 2778) a.Manifest = "" - apkVersion, err := a.getBuildVersion(h.env) + apkVersion, err := a.getBuildVersion(h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, apkVersion, 2778) } diff --git a/deploy_cmd.go b/parsecmd/deploy.go similarity index 93% rename from deploy_cmd.go rename to parsecmd/deploy.go index 702c667..c443a70 100644 --- a/deploy_cmd.go +++ b/parsecmd/deploy.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "crypto/md5" @@ -16,6 +16,7 @@ import ( "sync" "time" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/errgroup" "github.com/facebookgo/jsonpipe" "github.com/facebookgo/stackerr" @@ -39,7 +40,7 @@ type deployCmd struct { func (d *deployCmd) getSourceFiles( dirName string, suffixes map[string]struct{}, - e *env, + e *parsecli.Env, ) ([]string, []string, error) { ignoreFile := filepath.Join(e.Root, parseIgnore) @@ -158,7 +159,7 @@ func (d *deployCmd) computeChecksums(files []string, return checksums, nil } -func (d *deployCmd) uploadFile(filename, endpoint string, e *env, +func (d *deployCmd) uploadFile(filename, endpoint string, e *parsecli.Env, normalizeName func(string) string) (string, error) { content, err := ioutil.ReadFile(filename) if err != nil { @@ -205,7 +206,7 @@ type uploader struct { DirName string Suffixes map[string]struct{} EndPoint string - Env *env + Env *parsecli.Env PrevChecksums map[string]string PrevVersions map[string]string } @@ -312,7 +313,7 @@ type deployInfo struct { Warning string `json:"warning,omitempty"` // only populated by post to deploy } -func (d *deployCmd) makeNewRelease(info *deployInfo, e *env) (deployInfo, error) { +func (d *deployCmd) makeNewRelease(info *deployInfo, e *parsecli.Env) (deployInfo, error) { var res deployInfo u := url.URL{ Path: "deploy", @@ -324,7 +325,7 @@ func (d *deployCmd) makeNewRelease(info *deployInfo, e *env) (deployInfo, error) return res, nil } -func (d *deployCmd) getPrevDeplInfo(e *env) (*deployInfo, error) { +func (d *deployCmd) getPrevDeplInfo(e *parsecli.Env) (*deployInfo, error) { prevDeplInfo := &deployInfo{} if _, err := e.ParseAPIClient.Get(&url.URL{Path: "deploy"}, prevDeplInfo); err != nil { return nil, stackerr.Wrap(err) @@ -357,7 +358,7 @@ func (d *deployCmd) deploy( parseVersion string, prevDeplInfo *deployInfo, forDevelop bool, - e *env) (*deployInfo, error) { + e *parsecli.Env) (*deployInfo, error) { if parseVersion == "" { fmt.Fprintln(e.Err, "JS SDK version not set, setting it to latest available JS SDK version", @@ -468,7 +469,7 @@ func (d *deployCmd) deploy( func (d *deployCmd) handleError( n int, err, prevErr error, - e *env, + e *parsecli.Env, ) error { if err == nil { return nil @@ -482,9 +483,9 @@ func (d *deployCmd) handleError( waitTime = d.wait(n) } - errStr := errorString(e, err) + errStr := parsecli.ErrorString(e, err) if prevErr != nil { - prevErrStr := errorString(e, prevErr) + prevErrStr := parsecli.ErrorString(e, prevErr) if prevErrStr == errStr { fmt.Fprintf( e.Err, @@ -506,15 +507,15 @@ func (d *deployCmd) handleError( return nil } -func (d *deployCmd) run(e *env, c *context) error { +func (d *deployCmd) run(e *parsecli.Env, c *parsecli.Context) error { var prevErr error for i := 0; i < d.Retries; i++ { - parseVersion := c.Config.getProjectConfig().Parse.JSSDK + parseVersion := c.Config.GetProjectConfig().Parse.JSSDK newDeployInfo, err := d.deploy(parseVersion, nil, false, e) if err == nil { if parseVersion == "" && newDeployInfo != nil && newDeployInfo.ParseVersion != "" { - c.Config.getProjectConfig().Parse.JSSDK = newDeployInfo.ParseVersion - return storeProjectConfig(e, c.Config) + c.Config.GetProjectConfig().Parse.JSSDK = newDeployInfo.ParseVersion + return parsecli.StoreProjectConfig(e, c.Config) } return nil } @@ -527,7 +528,7 @@ func (d *deployCmd) run(e *env, c *context) error { return nil } -func newDeployCmd(e *env) *cobra.Command { +func NewDeployCmd(e *parsecli.Env) *cobra.Command { d := deployCmd{ Verbose: true, Retries: 3, @@ -538,7 +539,7 @@ func newDeployCmd(e *env) *cobra.Command { Use: "deploy [app]", Short: "Deploys a Parse App", Long: `Deploys the code to the given app.`, - Run: runWithClient(e, d.run), + Run: parsecli.RunWithClient(e, d.run), } cmd.Flags().StringVarP(&d.Description, "description", "d", d.Description, "Add an optional description to the deploy") diff --git a/deploy_cmd_test.go b/parsecmd/deploy_test.go similarity index 71% rename from deploy_cmd_test.go rename to parsecmd/deploy_test.go index 734d85d..6628aae 100644 --- a/deploy_cmd_test.go +++ b/parsecmd/deploy_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "fmt" @@ -11,24 +11,25 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" ) -func createParseProject(t testing.TB) *Harness { - h := newHarness(t) - h.makeEmptyRoot() +func createParseProject(t testing.TB) *parsecli.Harness { + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, "parse"), 0755)) - h.env.Root = filepath.Join(h.env.Root, "parse") + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse"), 0755)) + h.Env.Root = filepath.Join(h.Env.Root, "parse") - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, cloudDir), 0755)) - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, hostingDir), 0755)) - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, configDir), 0755)) + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, parsecli.CloudDir), 0755)) + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, parsecli.HostingDir), 0755)) + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) ensure.Nil(t, ioutil.WriteFile( - filepath.Join(h.env.Root, hostingDir, "index.html"), + filepath.Join(h.Env.Root, parsecli.HostingDir, "index.html"), []byte(` Parse Project `), @@ -37,13 +38,13 @@ func createParseProject(t testing.TB) *Harness { ensure.Nil(t, ioutil.WriteFile( - filepath.Join(h.env.Root, cloudDir, "main.js"), + filepath.Join(h.Env.Root, parsecli.CloudDir, "main.js"), []byte(`echo {"success": "ok"}`), 0600), ) ensure.Nil(t, ioutil.WriteFile( - filepath.Join(h.env.Root, parseIgnore), + filepath.Join(h.Env.Root, parseIgnore), []byte(` *.swp *~ @@ -53,7 +54,7 @@ func createParseProject(t testing.TB) *Harness { ensure.Nil(t, ioutil.WriteFile( - filepath.Join(h.env.Root, legacyConfigFile), + filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), []byte(`{ "global": {"parseVersion": "1.2.9"} } @@ -63,10 +64,10 @@ func createParseProject(t testing.TB) *Harness { ) ignoredFiles := []string{ - filepath.Join(h.env.Root, "public", ".ignore"), - filepath.Join(h.env.Root, "public", "#ignore"), - filepath.Join(h.env.Root, "cloud", "sample.txt"), - filepath.Join(h.env.Root, "cloud", "test~"), + filepath.Join(h.Env.Root, "public", ".ignore"), + filepath.Join(h.Env.Root, "public", "#ignore"), + filepath.Join(h.Env.Root, "cloud", "sample.txt"), + filepath.Join(h.Env.Root, "cloud", "test~"), } for _, ignoredFile := range ignoredFiles { @@ -78,10 +79,10 @@ func createParseProject(t testing.TB) *Harness { return h } -func setupForDeploy(t testing.TB, info *deployInfo) *Harness { +func setupForDeploy(t testing.TB, info *deployInfo) *parsecli.Harness { h := createParseProject(t) - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { switch r.URL.Path { case "/1/deploy": return &http.Response{ @@ -105,36 +106,36 @@ func setupForDeploy(t testing.TB, info *deployInfo) *Harness { }, nil } }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} return h } func TestGenericGetSourceFiles(t *testing.T) { t.Parallel() - h := newHarness(t) - h.makeEmptyRoot() + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() defer h.Stop() - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, "src"), 0755)) + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "src"), 0755)) for _, name := range []string{"index.html"} { - file, err := os.Create(filepath.Join(h.env.Root, "src", name)) + file, err := os.Create(filepath.Join(h.Env.Root, "src", name)) ensure.Nil(t, err) ensure.Nil(t, file.Close()) } - ensure.Nil(t, os.Symlink(filepath.Join(h.env.Root, "src"), filepath.Join(h.env.Root, "public"))) + ensure.Nil(t, os.Symlink(filepath.Join(h.Env.Root, "src"), filepath.Join(h.Env.Root, "public"))) var d deployCmd files, ignored, err := d.getSourceFiles( - filepath.Join(h.env.Root, "public"), + filepath.Join(h.Env.Root, "public"), map[string]struct{}{".html": {}}, - h.env, + h.Env, ) ensure.Nil(t, err) - ensure.DeepEqual(t, files, []string{filepath.Join(h.env.Root, "public", "index.html")}) + ensure.DeepEqual(t, files, []string{filepath.Join(h.Env.Root, "public", "index.html")}) ensure.DeepEqual(t, len(ignored), 0) } @@ -144,13 +145,13 @@ func TestComputeChecksums(t *testing.T) { defer h.Stop() files := []string{ - filepath.Join(h.env.Root, "cloud", "main.js"), - filepath.Join(h.env.Root, "public", "index.html"), + filepath.Join(h.Env.Root, "cloud", "main.js"), + filepath.Join(h.Env.Root, "public", "index.html"), } var d deployCmd - prefixLen := len(filepath.Join(h.env.Root, "1")) - 1 + prefixLen := len(filepath.Join(h.Env.Root, "1")) - 1 res, err := d.computeChecksums(files, func(name string) string { name = filepath.ToSlash(filepath.Clean(name)) return name[prefixLen:] @@ -163,11 +164,11 @@ func TestComputeChecksums(t *testing.T) { func TestUploadFileNoFile(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var d deployCmd - _, err := d.uploadFile("cloud/master.js", "", h.env, nil) + _, err := d.uploadFile("cloud/master.js", "", h.Env, nil) switch runtime.GOOS { case "windows": ensure.Err(t, err, regexp.MustCompile(`The system cannot find the path specified.`)) @@ -182,17 +183,17 @@ func TestUploadFileHttpError(t *testing.T) { defer h.Stop() var d deployCmd - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/uploads") return &http.Response{ StatusCode: http.StatusExpectationFailed, Body: ioutil.NopCloser(strings.NewReader(`{"error": "something is wrong"}`)), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - dirRoot := filepath.Join(h.env.Root, "cloud") + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + dirRoot := filepath.Join(h.Env.Root, "cloud") _, err := d.uploadFile(filepath.Join(dirRoot, "main.js"), "uploads", - h.env, func(name string) string { return "main.js" }) + h.Env, func(name string) string { return "main.js" }) ensure.Err(t, err, regexp.MustCompile("something is wrong")) } @@ -202,7 +203,7 @@ func TestUploadFileMalformed(t *testing.T) { defer h.Stop() var d deployCmd - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/uploads") return &http.Response{ StatusCode: http.StatusOK, @@ -210,16 +211,16 @@ func TestUploadFileMalformed(t *testing.T) { }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - dirRoot := filepath.Join(h.env.Root, "cloud") - _, err := d.uploadFile(filepath.Join(dirRoot, "main.js"), "uploads", h.env, + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + dirRoot := filepath.Join(h.Env.Root, "cloud") + _, err := d.uploadFile(filepath.Join(dirRoot, "main.js"), "uploads", h.Env, func(name string) string { return "main.js" }) ensure.Err(t, err, regexp.MustCompile(`Malformed response when trying to upload `)) } func TestMakeNewRelease(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var d deployCmd @@ -235,40 +236,40 @@ func TestMakeNewRelease(t *testing.T) { Public: map[string]string{"index.html": "f2"}, }, } - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/deploy") return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &info))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - res, err := d.makeNewRelease(&deployInfo{}, h.env) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + res, err := d.makeNewRelease(&deployInfo{}, h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, info, res) } func TestMakeNewReleaseError(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var d deployCmd - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/deploy") return &http.Response{ StatusCode: http.StatusExpectationFailed, Body: ioutil.NopCloser(strings.NewReader(`{"error": "something is wrong"}`)), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - _, err := d.makeNewRelease(&deployInfo{}, h.env) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + _, err := d.makeNewRelease(&deployInfo{}, h.Env) ensure.Err(t, err, regexp.MustCompile("something is wrong")) } func TestGetPrevDeplInfo(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var d deployCmd @@ -284,22 +285,22 @@ func TestGetPrevDeplInfo(t *testing.T) { Public: map[string]string{"index.html": "f1"}, }, } - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/deploy") return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, info))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - res, err := d.getPrevDeplInfo(h.env) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + res, err := d.getPrevDeplInfo(h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, res, info) } func TestGetPrevDeplInfoLegacy(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var d deployCmd @@ -316,15 +317,15 @@ func TestGetPrevDeplInfoLegacy(t *testing.T) { Checksums: map[string]string{"main.js": "d41d8cd98f00b204e9800998ecf8427e"}, Versions: map[string]string{"main.js": "f1"}, } - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/deploy") return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, info))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - res, err := d.getPrevDeplInfo(h.env) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + res, err := d.getPrevDeplInfo(h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, res, &deployInfo{ ReleaseName: "v1", @@ -340,19 +341,19 @@ func TestGetPrevDeplInfoLegacy(t *testing.T) { func TestGetPrevDeplInfoError(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var d deployCmd - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/deploy") return &http.Response{ StatusCode: http.StatusExpectationFailed, Body: ioutil.NopCloser(strings.NewReader(`{"error": "something is wrong"}`)), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - _, err := d.getPrevDeplInfo(h.env) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + _, err := d.getPrevDeplInfo(h.Env) ensure.Err(t, err, regexp.MustCompile("something is wrong")) } @@ -365,7 +366,7 @@ func TestUploadSourceFilesChanged(t *testing.T) { DirName: "cloud", Suffixes: map[string]struct{}{".js": {}}, EndPoint: "uploads", - Env: h.env, + Env: h.Env, PrevChecksums: map[string]string{ "main.js": "d41d8cd98f00b204e9800998ecf8427e", }, @@ -374,7 +375,7 @@ func TestUploadSourceFilesChanged(t *testing.T) { }, } - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/uploads") return &http.Response{ StatusCode: http.StatusOK, @@ -382,7 +383,7 @@ func TestUploadSourceFilesChanged(t *testing.T) { }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} var d deployCmd checksums, versions, err := d.uploadSourceFiles(u) @@ -412,7 +413,7 @@ func TestUploadSourceFilesUnChanged(t *testing.T) { DirName: "cloud", Suffixes: map[string]struct{}{".js": {}}, EndPoint: "scripts", - Env: h.env, + Env: h.Env, PrevChecksums: map[string]string{ "main.js": "4ece160cc8e5e828ee718e7367cf5d37", }, @@ -453,7 +454,7 @@ func TestDeployFilesChanged(t *testing.T) { defer h.Stop() d := deployCmd{Verbose: true} - res, err := d.deploy("old", nil, false, h.env) + res, err := d.deploy("old", nil, false, h.Env) ensure.Nil(t, err) expected := &deployInfo{ @@ -484,15 +485,15 @@ The following files will be ignored: Finished uploading files New release is named v1 (using Parse JavaScript SDK vlatest) `, - filepath.Join(h.env.Root, cloudDir, "main.js"), + filepath.Join(h.Env.Root, parsecli.CloudDir, "main.js"), strings.Join([]string{ - filepath.Join(h.env.Root, cloudDir, "sample.txt"), - filepath.Join(h.env.Root, cloudDir, "test~")}, + filepath.Join(h.Env.Root, parsecli.CloudDir, "sample.txt"), + filepath.Join(h.Env.Root, parsecli.CloudDir, "test~")}, "\n"), - filepath.Join(h.env.Root, hostingDir, "index.html"), + filepath.Join(h.Env.Root, parsecli.HostingDir, "index.html"), strings.Join([]string{ - filepath.Join(h.env.Root, hostingDir, "#ignore"), - filepath.Join(h.env.Root, hostingDir, ".ignore")}, + filepath.Join(h.Env.Root, parsecli.HostingDir, "#ignore"), + filepath.Join(h.Env.Root, parsecli.HostingDir, ".ignore")}, "\n"), ), ) @@ -516,7 +517,7 @@ func TestDeployFilesUnChanged(t *testing.T) { defer h.Stop() d := deployCmd{Verbose: true} - res, err := d.deploy("latest", nil, false, h.env) + res, err := d.deploy("latest", nil, false, h.Env) ensure.Nil(t, err) expected := &deployInfo{ @@ -556,7 +557,7 @@ func TestDeployFilesNoVersion(t *testing.T) { defer h.Stop() d := deployCmd{Verbose: true} - res, err := d.deploy("", nil, false, h.env) + res, err := d.deploy("", nil, false, h.Env) ensure.Nil(t, err) expected := &deployInfo{ @@ -588,24 +589,24 @@ The following files will be ignored: Finished uploading files New release is named v1 (using Parse JavaScript SDK vlatest) `, - filepath.Join(h.env.Root, cloudDir, "main.js"), + filepath.Join(h.Env.Root, parsecli.CloudDir, "main.js"), strings.Join([]string{ - filepath.Join(h.env.Root, cloudDir, "sample.txt"), - filepath.Join(h.env.Root, cloudDir, "test~")}, + filepath.Join(h.Env.Root, parsecli.CloudDir, "sample.txt"), + filepath.Join(h.Env.Root, parsecli.CloudDir, "test~")}, "\n"), - filepath.Join(h.env.Root, hostingDir, "index.html"), + filepath.Join(h.Env.Root, parsecli.HostingDir, "index.html"), strings.Join([]string{ - filepath.Join(h.env.Root, hostingDir, "#ignore"), - filepath.Join(h.env.Root, hostingDir, ".ignore")}, + filepath.Join(h.Env.Root, parsecli.HostingDir, "#ignore"), + filepath.Join(h.Env.Root, parsecli.HostingDir, ".ignore")}, "\n"), ), ) - c, err := configFromDir(h.env.Root) + c, err := parsecli.ConfigFromDir(h.Env.Root) ensure.Nil(t, err) - config, ok := (c).(*parseConfig) + config, ok := (c).(*parsecli.ParseConfig) ensure.True(t, ok) - ensure.DeepEqual(t, config.projectConfig.Parse.JSSDK, "2.0") + ensure.DeepEqual(t, config.ProjectConfig.Parse.JSSDK, "2.0") ensure.True(t, strings.Contains(h.Err.String(), `JS SDK version not set, setting it to latest available JS SDK version `), ) @@ -614,7 +615,7 @@ New release is named v1 (using Parse JavaScript SDK vlatest) func TestDeployRetries(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() info := &struct { @@ -629,25 +630,25 @@ func TestDeployRetries(t *testing.T) { Checksums: map[string]string{"main.js": "d41d8cd98f00b204e9800998ecf8427e"}, Versions: map[string]string{"main.js": "f1"}, } - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/deploy") return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, info))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} d := &deployCmd{Retries: 1} - ctx := context{Config: defaultParseConfig} - ctx.Config.getProjectConfig().Parse.JSSDK = "latest" + ctx := parsecli.Context{Config: defaultParseConfig} + ctx.Config.GetProjectConfig().Parse.JSSDK = "latest" - ensure.Err(t, d.run(h.env, &ctx), regexp.MustCompile("no such file or directory")) + ensure.Err(t, d.run(h.Env, &ctx), regexp.MustCompile("no such file or directory")) ensure.DeepEqual(t, h.Err.String(), "") h.Err.Reset() d.Retries = 2 - ensure.Err(t, d.run(h.env, &ctx), regexp.MustCompile("no such file or directory")) + ensure.Err(t, d.run(h.Env, &ctx), regexp.MustCompile("no such file or directory")) ensure.DeepEqual( t, h.Err.String(), @@ -656,7 +657,7 @@ func TestDeployRetries(t *testing.T) { h.Err.Reset() d.Retries = 5 - ensure.Err(t, d.run(h.env, &ctx), regexp.MustCompile("no such file or directory")) + ensure.Err(t, d.run(h.Env, &ctx), regexp.MustCompile("no such file or directory")) errStr := "Deploy failed with error:\nlstat cloud: no such file or directory\nWill retry in 0 seconds.\n\n" errStr += strings.Repeat("Sorry, deploy failed again with same error.\nWill retry in 0 seconds.\n\n", 3) ensure.DeepEqual(t, h.Err.String(), errStr) @@ -668,18 +669,18 @@ func TestIgnoredFiles(t *testing.T) { defer h.Stop() d := deployCmd{Verbose: true} - _, ignored, err := d.getSourceFiles(filepath.Join(h.env.Root, cloudDir), map[string]struct{}{}, h.env) + _, ignored, err := d.getSourceFiles(filepath.Join(h.Env.Root, parsecli.CloudDir), map[string]struct{}{}, h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, ignored, []string{ - filepath.Join(h.env.Root, cloudDir, "test~")}) - _, ignored, err = d.getSourceFiles(filepath.Join(h.env.Root, hostingDir), map[string]struct{}{}, h.env) + filepath.Join(h.Env.Root, parsecli.CloudDir, "test~")}) + _, ignored, err = d.getSourceFiles(filepath.Join(h.Env.Root, parsecli.HostingDir), map[string]struct{}{}, h.Env) ensure.DeepEqual(t, ignored, []string{ - filepath.Join(h.env.Root, hostingDir, "#ignore"), - filepath.Join(h.env.Root, hostingDir, ".ignore"), + filepath.Join(h.Env.Root, parsecli.HostingDir, "#ignore"), + filepath.Join(h.Env.Root, parsecli.HostingDir, ".ignore"), }) } @@ -691,17 +692,17 @@ func TestIgnoredFilesUnderDotDir(t *testing.T) { d := deployCmd{Verbose: true} - _, ignored, err := d.getSourceFiles(filepath.Join(h.env.Root, cloudDir), map[string]struct{}{}, h.env) + _, ignored, err := d.getSourceFiles(filepath.Join(h.Env.Root, parsecli.CloudDir), map[string]struct{}{}, h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, ignored, []string{ - filepath.Join(h.env.Root, cloudDir, "test~")}) - _, ignored, err = d.getSourceFiles(filepath.Join(h.env.Root, hostingDir), map[string]struct{}{}, h.env) + filepath.Join(h.Env.Root, parsecli.CloudDir, "test~")}) + _, ignored, err = d.getSourceFiles(filepath.Join(h.Env.Root, parsecli.HostingDir), map[string]struct{}{}, h.Env) ensure.DeepEqual(t, ignored, []string{ - filepath.Join(h.env.Root, hostingDir, "#ignore"), - filepath.Join(h.env.Root, hostingDir, ".ignore"), + filepath.Join(h.Env.Root, parsecli.HostingDir, "#ignore"), + filepath.Join(h.Env.Root, parsecli.HostingDir, ".ignore"), }) } diff --git a/develop_cmd.go b/parsecmd/develop.go similarity index 83% rename from develop_cmd.go rename to parsecmd/develop.go index 7831375..457b961 100644 --- a/develop_cmd.go +++ b/parsecmd/develop.go @@ -1,10 +1,11 @@ -package main +package parsecmd import ( "fmt" "net" "time" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/spf13/cobra" ) @@ -24,9 +25,9 @@ type developCmd struct { type deployFunc func(parseVersion string, prevDeplInfo *deployInfo, forDevelop bool, - e *env) (*deployInfo, error) + e *parsecli.Env) (*deployInfo, error) -func (d *developCmd) contDeploy(e *env, deployer deployFunc, first, done chan struct{}) { +func (d *developCmd) contDeploy(e *parsecli.Env, deployer deployFunc, first, done chan struct{}) { if d.deployInterval == 0 { d.deployInterval = time.Second } @@ -42,7 +43,7 @@ func (d *developCmd) contDeploy(e *env, deployer deployFunc, first, done chan st case <-done: return } - config, err := configFromDir(e.Root) + config, err := parsecli.ConfigFromDir(e.Root) if err != nil { if !latestError { latestError = true @@ -53,7 +54,7 @@ func (d *developCmd) contDeploy(e *env, deployer deployFunc, first, done chan st `Config malformed. Please fix your config file in %s and try again. `, - getConfigFile(e), + parsecli.GetConfigFile(e), ) } if first != nil { // first deploy aborted @@ -63,7 +64,7 @@ Please fix your config file in %s and try again. continue } latestError = false - newDeplInfo, _ := deployer(config.getProjectConfig().Parse.JSSDK, prevDeplInfo, true, e) + newDeplInfo, _ := deployer(config.GetProjectConfig().Parse.JSSDK, prevDeplInfo, true, e) if !d.mustFetch { prevDeplInfo = newDeplInfo } @@ -74,7 +75,7 @@ Please fix your config file in %s and try again. } } -func (d *developCmd) handleError(e *env, err error, sleep func(time.Duration)) error { +func (d *developCmd) handleError(e *parsecli.Env, err error, sleep func(time.Duration)) error { if err == nil { return nil } @@ -92,7 +93,7 @@ func (d *developCmd) handleError(e *env, err error, sleep func(time.Duration)) e return err } -func (d *developCmd) run(e *env, c *context) error { +func (d *developCmd) run(e *parsecli.Env, c *parsecli.Context) error { first := make(chan struct{}) go d.contDeploy(e, deployFunc((&deployCmd{Verbose: d.Verbose}).deploy), @@ -121,7 +122,7 @@ func (d *developCmd) run(e *env, c *context) error { return err } -func newDevelopCmd(e *env) *cobra.Command { +func NewDevelopCmd(e *parsecli.Env) *cobra.Command { d := &developCmd{deployInterval: time.Second} cmd := &cobra.Command{ Use: "develop app", @@ -130,7 +131,7 @@ func newDevelopCmd(e *env) *cobra.Command { `This will also monitor the parse INFO log for any new log messages and write ` + `out updates to the terminal. This requires an app to be provided, to ` + `avoid running develop on production apps accidently.`, - Run: runWithAppClient(e, d.run), + Run: parsecli.RunWithAppClient(e, d.run), } cmd.Flags().DurationVarP(&d.deployInterval, "interval", "i", d.deployInterval, "Number of seconds between deploys.") cmd.Flags().BoolVarP(&d.mustFetch, "fetch", "f", d.mustFetch, "Always fetch previous deployment info from server") diff --git a/develop_cmd_test.go b/parsecmd/develop_test.go similarity index 72% rename from develop_cmd_test.go rename to parsecmd/develop_test.go index 61adca4..11ad7d0 100644 --- a/develop_cmd_test.go +++ b/parsecmd/develop_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "errors" @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) @@ -21,7 +22,7 @@ func TestContDeploy(t *testing.T) { deployer := deployFunc(func(parseVersion string, prevDeplInfo *deployInfo, forDevelop bool, - e *env) (*deployInfo, error) { + e *parsecli.Env) (*deployInfo, error) { return &deployInfo{}, nil }) @@ -32,7 +33,7 @@ func TestContDeploy(t *testing.T) { }() first := make(chan struct{}) - (&developCmd{}).contDeploy(h.env, deployer, first, done) + (&developCmd{}).contDeploy(h.Env, deployer, first, done) _, opened := <-first ensure.False(t, opened) } @@ -45,17 +46,17 @@ func TestContDeployConfigErr(t *testing.T) { ensure.Nil(t, ioutil.WriteFile( - filepath.Join(h.env.Root, legacyConfigFile), + filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), []byte("}"), 0600, ), ) - h.env.Type = legacyParseFormat + h.Env.Type = parsecli.LegacyParseFormat deployer := deployFunc(func(parseVersion string, prevDeplInfo *deployInfo, forDevelop bool, - e *env) (*deployInfo, error) { + e *parsecli.Env) (*deployInfo, error) { return &deployInfo{}, nil }) @@ -66,7 +67,7 @@ func TestContDeployConfigErr(t *testing.T) { }() first := make(chan struct{}) - (&developCmd{}).contDeploy(h.env, deployer, first, done) + (&developCmd{}).contDeploy(h.Env, deployer, first, done) _, opened := <-first ensure.False(t, opened) @@ -77,25 +78,25 @@ func TestContDeployConfigErr(t *testing.T) { `Config malformed. Please fix your config file in %s and try again. `, - filepath.Join(h.env.Root, legacyConfigFile), + filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), ), ) } func TestHandleError(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() d := &developCmd{} netErr := &net.OpError{Err: errors.New("network")} sleep := func(d time.Duration) {} - ensure.DeepEqual(t, d.handleError(h.env, netErr, sleep), netErr) + ensure.DeepEqual(t, d.handleError(h.Env, netErr, sleep), netErr) ensure.DeepEqual(t, h.Err.String(), "Flaky network. Waiting 20s before trying to fetch logs again.") h.Out.Reset() h.Err.Reset() otherErr := errors.New("other") - ensure.DeepEqual(t, d.handleError(h.env, otherErr, sleep), otherErr) + ensure.DeepEqual(t, d.handleError(h.Env, otherErr, sleep), otherErr) ensure.DeepEqual(t, h.Err.Len(), 0) } diff --git a/download_cmd.go b/parsecmd/download.go similarity index 91% rename from download_cmd.go rename to parsecmd/download.go index 4bf6d4d..ca8b3e7 100644 --- a/download_cmd.go +++ b/parsecmd/download.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "crypto/md5" @@ -12,6 +12,7 @@ import ( "path/filepath" "sync/atomic" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/errgroup" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" @@ -47,7 +48,7 @@ func (d *downloadCmd) verifyChecksum(path, checksum string) error { } func (d *downloadCmd) moveFiles( - e *env, + e *parsecli.Env, destination string, release *deployInfo) error { var wg errgroup.Group @@ -98,7 +99,7 @@ func (d *downloadCmd) moveFiles( maxParallel <- struct{}{} go moveFile( destination, - cloudDir, + parsecli.CloudDir, file, checksum, ) @@ -107,7 +108,7 @@ func (d *downloadCmd) moveFiles( maxParallel <- struct{}{} go moveFile( destination, - hostingDir, + parsecli.HostingDir, file, checksum, ) @@ -145,7 +146,7 @@ the temporary download location. return nil } -func (d *downloadCmd) download(e *env, destination string, release *deployInfo) error { +func (d *downloadCmd) download(e *parsecli.Env, destination string, release *deployInfo) error { var wg errgroup.Group maxParallel := make(chan struct{}, maxOpenFD) wg.Add(len(release.Versions.Cloud) + len(release.Versions.Public)) @@ -170,7 +171,7 @@ func (d *downloadCmd) download(e *env, destination string, release *deployInfo) return } - path := path.Join(destination, hostingDir, file) + path := path.Join(destination, parsecli.HostingDir, file) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { wg.Error(stackerr.Wrap(err)) return @@ -205,7 +206,7 @@ func (d *downloadCmd) download(e *env, destination string, release *deployInfo) return } - path := path.Join(destination, cloudDir, file) + path := path.Join(destination, parsecli.CloudDir, file) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { wg.Error(stackerr.Wrap(err)) return @@ -237,7 +238,7 @@ func (d *downloadCmd) download(e *env, destination string, release *deployInfo) return wg.Wait() } -func (d *downloadCmd) run(e *env, c *context) error { +func (d *downloadCmd) run(e *parsecli.Env, c *parsecli.Context) error { var err error latestRelease := d.release @@ -272,7 +273,7 @@ func (d *downloadCmd) run(e *env, c *context) error { return stackerr.Wrap(d.moveFiles(e, destination, latestRelease)) } -func newDownloadCmd(e *env) *cobra.Command { +func NewDownloadCmd(e *parsecli.Env) *cobra.Command { d := &downloadCmd{} cmd := &cobra.Command{ Use: "download [app]", @@ -280,7 +281,7 @@ func newDownloadCmd(e *env) *cobra.Command { Long: `Downloads the Cloud Code project at a given location, or at a temporary location if nothing is explicitly provided through the -l flag. `, - Run: runWithClient(e, d.run), + Run: parsecli.RunWithClient(e, d.run), } cmd.Flags().BoolVarP(&d.force, "force", "f", d.force, "Force will overwrite any files in the current project directory") diff --git a/download_cmd_test.go b/parsecmd/download_test.go similarity index 73% rename from download_cmd_test.go rename to parsecmd/download_test.go index 158cd94..42dbde8 100644 --- a/download_cmd_test.go +++ b/parsecmd/download_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "io/ioutil" @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" ) @@ -18,9 +19,9 @@ var ( hostedPath = regexp.MustCompile("/1/hosted_files/.*") ) -func newDownloadHarness(t testing.TB) (*Harness, *downloadCmd) { +func newDownloadHarness(t testing.TB) (*parsecli.Harness, *downloadCmd) { h := createParseProject(t) - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.FormValue("version"), "version") switch { case scriptPath.MatchString(r.URL.Path): @@ -41,8 +42,8 @@ func newDownloadHarness(t testing.TB) (*Harness, *downloadCmd) { } }) - h.makeEmptyRoot() - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.MakeEmptyRoot() + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} d := &downloadCmd{ release: &deployInfo{ @@ -72,14 +73,14 @@ func TestDownload(t *testing.T) { os.RemoveAll(tempDir) }() - err = d.download(h.env, tempDir, d.release) + err = d.download(h.Env, tempDir, d.release) ensure.Nil(t, err) for file := range d.release.Versions.Cloud { - _, err := os.Open(filepath.Join(tempDir, cloudDir, file)) + _, err := os.Open(filepath.Join(tempDir, parsecli.CloudDir, file)) ensure.Nil(t, err) } for file := range d.release.Versions.Public { - _, err := os.Open(filepath.Join(tempDir, hostingDir, file)) + _, err := os.Open(filepath.Join(tempDir, parsecli.HostingDir, file)) ensure.Nil(t, err) } } @@ -99,38 +100,38 @@ func TestDownloadMoveFiles(t *testing.T) { }() err = os.MkdirAll( - filepath.Join(tempDir, hostingDir), + filepath.Join(tempDir, parsecli.HostingDir), 0755, ) ensure.Nil(t, err) err = os.MkdirAll( - filepath.Join(tempDir, cloudDir), + filepath.Join(tempDir, parsecli.CloudDir), 0755, ) ensure.Nil(t, err) err = ioutil.WriteFile( - filepath.Join(tempDir, hostingDir, "index.html"), + filepath.Join(tempDir, parsecli.HostingDir, "index.html"), content, 0644, ) ensure.Nil(t, err) err = ioutil.WriteFile( - filepath.Join(tempDir, cloudDir, "main.js"), + filepath.Join(tempDir, parsecli.CloudDir, "main.js"), content, 0644, ) ensure.Nil(t, err) - err = d.moveFiles(h.env, tempDir, d.release) + err = d.moveFiles(h.Env, tempDir, d.release) ensure.Nil(t, err) readData, err := ioutil.ReadFile( - filepath.Join(h.env.Root, hostingDir, "index.html"), + filepath.Join(h.Env.Root, parsecli.HostingDir, "index.html"), ) ensure.DeepEqual(t, readData, content) readData, err = ioutil.ReadFile( - filepath.Join(h.env.Root, cloudDir, "main.js"), + filepath.Join(h.Env.Root, parsecli.CloudDir, "main.js"), ) ensure.DeepEqual(t, readData, content) } diff --git a/generate_cmd.go b/parsecmd/generate.go similarity index 92% rename from generate_cmd.go rename to parsecmd/generate.go index 461d15c..56c61e8 100644 --- a/generate_cmd.go +++ b/parsecmd/generate.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "bytes" @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -38,12 +39,12 @@ func (g *generateCmd) validateArgs() error { return nil } -func (g *generateCmd) run(e *env) error { +func (g *generateCmd) run(e *parsecli.Env) error { if err := g.validateArgs(); err != nil { return err } - destDir := filepath.Join(e.Root, cloudDir) + destDir := filepath.Join(e.Root, parsecli.CloudDir) existingFiles := false appJs := filepath.Join(destDir, "app.js") @@ -137,13 +138,13 @@ func (g *generateCmd) run(e *env) error { return nil } -func newGenerateCmd(e *env) *cobra.Command { +func NewGenerateCmd(e *parsecli.Env) *cobra.Command { c := generateCmd{} cmd := &cobra.Command{ Use: "generate", Short: "Generates a sample express app in the current project directory", Long: "Generates a sample express app in the current project directory.", - Run: runNoArgs(e, c.run), + Run: parsecli.RunNoArgs(e, c.run), } cmd.Flags().StringVarP(&c.generateType, "type", "t", expressEjs, "Type of templates to use for generation.") return cmd diff --git a/generate_cmd_test.go b/parsecmd/generate_test.go similarity index 76% rename from generate_cmd_test.go rename to parsecmd/generate_test.go index 0eeedb6..f53cbee 100644 --- a/generate_cmd_test.go +++ b/parsecmd/generate_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "fmt" @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) @@ -28,9 +29,9 @@ func TestGenerateCheckInvalidArgs(t *testing.T) { ensure.Err(t, err, regexp.MustCompile(`type can only be one of `)) } -func newGenerateCmdHarness(t testing.TB, generateType string) (*Harness, *generateCmd) { - h := newHarness(t) - h.makeEmptyRoot() +func newGenerateCmdHarness(t testing.TB, generateType string) (*parsecli.Harness, *generateCmd) { + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() g := &generateCmd{generateType} return h, g } @@ -39,7 +40,7 @@ func TestGenerateEjsOutput(t *testing.T) { t.Parallel() h, g := newGenerateCmdHarness(t, expressEjs) defer h.Stop() - g.run(h.env) + g.run(h.Env) output := fmt.Sprintf(`Creating directory %s. Writing out sample file %s @@ -49,9 +50,9 @@ Almost done! Please add this line to the top of your main.js: require('cloud/app.js'); -`, filepath.Join(h.env.Root, "cloud", "views"), - filepath.Join(h.env.Root, "cloud", "app.js"), - filepath.Join(h.env.Root, "cloud", "views", "hello.ejs")) +`, filepath.Join(h.Env.Root, "cloud", "views"), + filepath.Join(h.Env.Root, "cloud", "app.js"), + filepath.Join(h.Env.Root, "cloud", "views", "hello.ejs")) ensure.DeepEqual(t, h.Out.String(), output) ensure.DeepEqual(t, h.Err.String(), "") @@ -61,9 +62,9 @@ func TestGenerateEjs(t *testing.T) { t.Parallel() h, g := newGenerateCmdHarness(t, expressEjs) defer h.Stop() - g.run(h.env) + g.run(h.Env) - cloudRoot := filepath.Join(h.env.Root, cloudDir) + cloudRoot := filepath.Join(h.Env.Root, parsecli.CloudDir) appJs := filepath.Join(cloudRoot, "app.js") content, err := ioutil.ReadFile(appJs) @@ -79,16 +80,16 @@ func TestGenerateEjsExists(t *testing.T) { t.Parallel() h, g := newGenerateCmdHarness(t, expressEjs) defer h.Stop() - g.run(h.env) + g.run(h.Env) - ensure.Err(t, g.run(h.env), regexp.MustCompile("Please remove the above existing files and try again.")) + ensure.Err(t, g.run(h.Env), regexp.MustCompile("Please remove the above existing files and try again.")) } func TestGenerateJadeOutput(t *testing.T) { t.Parallel() h, g := newGenerateCmdHarness(t, expressJade) defer h.Stop() - g.run(h.env) + g.run(h.Env) output := fmt.Sprintf(`Creating directory %s. Writing out sample file %s @@ -98,9 +99,9 @@ Almost done! Please add this line to the top of your main.js: require('cloud/app.js'); -`, filepath.Join(h.env.Root, "cloud", "views"), - filepath.Join(h.env.Root, "cloud", "app.js"), - filepath.Join(h.env.Root, "cloud", "views", "hello.jade")) +`, filepath.Join(h.Env.Root, "cloud", "views"), + filepath.Join(h.Env.Root, "cloud", "app.js"), + filepath.Join(h.Env.Root, "cloud", "views", "hello.jade")) ensure.DeepEqual(t, h.Out.String(), output) ensure.DeepEqual(t, h.Err.String(), "") @@ -110,9 +111,9 @@ func TestGenerateJade(t *testing.T) { t.Parallel() h, g := newGenerateCmdHarness(t, expressJade) defer h.Stop() - g.run(h.env) + g.run(h.Env) - cloudRoot := filepath.Join(h.env.Root, cloudDir) + cloudRoot := filepath.Join(h.Env.Root, parsecli.CloudDir) appJs := filepath.Join(cloudRoot, "app.js") content, err := ioutil.ReadFile(appJs) @@ -128,7 +129,7 @@ func TestGenerateJadeExists(t *testing.T) { t.Parallel() h, g := newGenerateCmdHarness(t, expressJade) defer h.Stop() - g.run(h.env) + g.run(h.Env) - ensure.Err(t, g.run(h.env), regexp.MustCompile("Please remove the above existing files and try again.")) + ensure.Err(t, g.run(h.Env), regexp.MustCompile("Please remove the above existing files and try again.")) } diff --git a/ios_symbol_uploader.go b/parsecmd/ios_symbol_uploader.go similarity index 95% rename from ios_symbol_uploader.go rename to parsecmd/ios_symbol_uploader.go index 2ccc31d..f022d7b 100644 --- a/ios_symbol_uploader.go +++ b/parsecmd/ios_symbol_uploader.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "bytes" @@ -10,6 +10,7 @@ import ( "runtime" "github.com/DHowett/go-plist" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/inconshreveable/go-update" "github.com/mitchellh/go-homedir" @@ -103,7 +104,7 @@ func (i *iosSymbolUploader) getBuildVersionFromXcarchive() (string, error) { return properties.ApplicationProperties.CFBundleVersion, nil } -func (i *iosSymbolUploader) uploadSymbols(e *env) error { +func (i *iosSymbolUploader) uploadSymbols(e *parsecli.Env) error { appBuildVersion, err := i.getBuildVersionFromXcarchive() if err != nil { return err @@ -128,7 +129,7 @@ func (i *iosSymbolUploader) uploadSymbols(e *env) error { }, true, e) } -func (i *iosSymbolUploader) prepareSymbolsFolder(folderPath string, e *env) error { +func (i *iosSymbolUploader) prepareSymbolsFolder(folderPath string, e *parsecli.Env) error { if err := os.RemoveAll(folderPath); err != nil { return stackerr.Wrap(err) } @@ -142,7 +143,7 @@ type updateTool func(string, string) error func (i *iosSymbolUploader) symbolConversionTool(baseDir string, updater updateTool, - e *env) (string, error) { + e *parsecli.Env) (string, error) { converter := os.Getenv("PARSE_SYMBOL_CONVERTER") if converter != "" { @@ -200,7 +201,7 @@ func (i *iosSymbolUploader) symbolConversionTool(baseDir string, return toolPath, nil } -func (i *iosSymbolUploader) convertSymbols(e *env) ([]string, error) { +func (i *iosSymbolUploader) convertSymbols(e *parsecli.Env) ([]string, error) { homedir, err := homedir.Dir() if err != nil { return nil, stackerr.Wrap(err) diff --git a/ios_symbol_uploader_test.go b/parsecmd/ios_symbol_uploader_test.go similarity index 78% rename from ios_symbol_uploader_test.go rename to parsecmd/ios_symbol_uploader_test.go index 3b28e8f..9ae5376 100644 --- a/ios_symbol_uploader_test.go +++ b/parsecmd/ios_symbol_uploader_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "os" @@ -7,6 +7,7 @@ import ( "runtime" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) @@ -55,15 +56,15 @@ func TestBuildVersionFromXArchive(t *testing.T) { func TestPrepareSymbolsFolder(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.makeEmptyRoot() + h.MakeEmptyRoot() createRandomFiles(t, h) var i iosSymbolUploader - ensure.Nil(t, i.prepareSymbolsFolder(h.env.Root, h.env)) - files, err := readDirNames(h.env.Root) + ensure.Nil(t, i.prepareSymbolsFolder(h.Env.Root, h.Env)) + files, err := readDirNames(h.Env.Root) ensure.Nil(t, err) ensure.DeepEqual(t, len(files), 0) } @@ -71,18 +72,18 @@ func TestPrepareSymbolsFolder(t *testing.T) { func TestSymbolConversionToolPath(t *testing.T) { t.Parallel() var i iosSymbolUploader - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.makeEmptyRoot() - ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, ".parse"), 0755)) - path, err := i.symbolConversionTool(h.env.Root, + h.MakeEmptyRoot() + ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, ".parse"), 0755)) + path, err := i.symbolConversionTool(h.Env.Root, func(path string, url string) error { return nil }, - h.env) + h.Env) ensure.Nil(t, err) - ensure.DeepEqual(t, path, filepath.Join(h.env.Root, ".parse", "parse_symbol_converter")) + ensure.DeepEqual(t, path, filepath.Join(h.Env.Root, ".parse", "parse_symbol_converter")) ensure.DeepEqual(t, h.Out.String(), `Additional resources installed. `) diff --git a/jssdk_cmd.go b/parsecmd/jssdk.go similarity index 66% rename from jssdk_cmd.go rename to parsecmd/jssdk.go index 90c053b..5dd74b3 100644 --- a/jssdk_cmd.go +++ b/parsecmd/jssdk.go @@ -1,10 +1,11 @@ -package main +package parsecmd import ( "fmt" "net/url" "sort" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -18,7 +19,7 @@ type jsSDKCmd struct { newVersion string } -func (j *jsSDKCmd) getAllJSSdks(e *env) ([]string, error) { +func (j *jsSDKCmd) getAllJSSdks(e *parsecli.Env) ([]string, error) { u := &url.URL{ Path: "jsVersions", } @@ -30,13 +31,13 @@ func (j *jsSDKCmd) getAllJSSdks(e *env) ([]string, error) { return response.JS, nil } -func (j *jsSDKCmd) printVersions(e *env, c *context) error { +func (j *jsSDKCmd) printVersions(e *parsecli.Env, c *parsecli.Context) error { allVersions, err := j.getAllJSSdks(e) if err != nil { return err } - currentVersion := c.Config.getProjectConfig().Parse.JSSDK + currentVersion := c.Config.GetProjectConfig().Parse.JSSDK for _, version := range allVersions { prefix := " " if currentVersion == version { @@ -47,19 +48,19 @@ func (j *jsSDKCmd) printVersions(e *env, c *context) error { return nil } -func (j *jsSDKCmd) getVersion(e *env, c *context) error { - if c.Config.getProjectConfig().Parse.JSSDK == "" { +func (j *jsSDKCmd) getVersion(e *parsecli.Env, c *parsecli.Context) error { + if c.Config.GetProjectConfig().Parse.JSSDK == "" { return stackerr.New("JavaScript SDK version not set for this project.") } fmt.Fprintf( e.Out, "Current JavaScript SDK version is %s\n", - c.Config.getProjectConfig().Parse.JSSDK, + c.Config.GetProjectConfig().Parse.JSSDK, ) return nil } -func (j *jsSDKCmd) setVersion(e *env, c *context) error { +func (j *jsSDKCmd) setVersion(e *parsecli.Env, c *parsecli.Context) error { allVersions, err := j.getAllJSSdks(e) if err != nil { return err @@ -74,22 +75,22 @@ func (j *jsSDKCmd) setVersion(e *env, c *context) error { return stackerr.New("Invalid SDK version selected.") } - conf, err := configFromDir(e.Root) + conf, err := parsecli.ConfigFromDir(e.Root) if err != nil { return err } - conf.getProjectConfig().Parse.JSSDK = j.newVersion - if err := storeProjectConfig(e, conf); err != nil { + conf.GetProjectConfig().Parse.JSSDK = j.newVersion + if err := parsecli.StoreProjectConfig(e, conf); err != nil { return err } - fmt.Fprintf(e.Out, "Current JavaScript SDK version is %s\n", conf.getProjectConfig().Parse.JSSDK) + fmt.Fprintf(e.Out, "Current JavaScript SDK version is %s\n", conf.GetProjectConfig().Parse.JSSDK) return nil } // useLatestJSSDK is a utility method used by deploy & develop // to write set jssdk version to latest available, if none set -func useLatestJSSDK(e *env) error { +func useLatestJSSDK(e *parsecli.Env) error { var j jsSDKCmd versions, err := j.getAllJSSdks(e) if err != nil { @@ -100,16 +101,16 @@ func useLatestJSSDK(e *env) error { return stackerr.New("No JavaScript SDK version is available.") } - config, err := configFromDir(e.Root) + config, err := parsecli.ConfigFromDir(e.Root) if err != nil { return err } - config.getProjectConfig().Parse.JSSDK = versions[0] - return storeProjectConfig(e, config) + config.GetProjectConfig().Parse.JSSDK = versions[0] + return parsecli.StoreProjectConfig(e, config) } -func (j *jsSDKCmd) run(e *env, c *context, args []string) error { +func (j *jsSDKCmd) run(e *parsecli.Env, c *parsecli.Context, args []string) error { switch { case j.all: return j.printVersions(e, c) @@ -122,14 +123,14 @@ func (j *jsSDKCmd) run(e *env, c *context, args []string) error { } } -func newJsSdkCmd(e *env) *cobra.Command { +func NewJsSdkCmd(e *parsecli.Env) *cobra.Command { j := jsSDKCmd{} cmd := &cobra.Command{ Use: "jssdk [version]", Short: "Sets the Parse JavaScript SDK version to use in Cloud Code", Long: `Sets the Parse JavaScript SDK version to use in Cloud Code. ` + `Prints the version number currently set if none is specified.`, - Run: runWithArgsClient(e, j.run), + Run: parsecli.RunWithArgsClient(e, j.run), } cmd.Flags().BoolVarP(&j.all, "all", "a", j.all, "Shows all available JavaScript SDK version") diff --git a/jssdk_cmd_test.go b/parsecmd/jssdk_test.go similarity index 56% rename from jssdk_cmd_test.go rename to parsecmd/jssdk_test.go index 4ca9e12..937f3b4 100644 --- a/jssdk_cmd_test.go +++ b/parsecmd/jssdk_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "io/ioutil" @@ -9,13 +9,21 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" ) -func newJsSdkHarness(t testing.TB) *Harness { - h := newHarness(t) - ht := transportFunc(func(r *http.Request) (*http.Response, error) { +var defaultParseConfig = &parsecli.ParseConfig{ + ProjectConfig: &parsecli.ProjectConfig{ + Type: parsecli.LegacyParseFormat, + Parse: &parsecli.ParseProjectConfig{}, + }, +} + +func newJsSdkHarness(t testing.TB) *parsecli.Harness { + h := parsecli.NewHarness(t) + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/jsVersions") rows := jsSDKVersion{JS: []string{"1.2.8", "1.2.9", "1.2.10", "1.2.11", "0.2.0"}} return &http.Response{ @@ -23,37 +31,37 @@ func newJsSdkHarness(t testing.TB) *Harness { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} return h } -func newJsSdkHarnessError(t testing.TB) *Harness { - h := newHarness(t) - ht := transportFunc(func(r *http.Request) (*http.Response, error) { +func newJsSdkHarnessError(t testing.TB) *parsecli.Harness { + h := parsecli.NewHarness(t) + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/jsVersions") return &http.Response{ StatusCode: http.StatusExpectationFailed, Body: ioutil.NopCloser(strings.NewReader(`{"error":"something is wrong"}`)), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} return h } -func newJsSdkHarnessWithConfig(t testing.TB) (*Harness, *context) { +func newJsSdkHarnessWithConfig(t testing.TB) (*parsecli.Harness, *parsecli.Context) { h := newJsSdkHarness(t) - h.makeEmptyRoot() + h.MakeEmptyRoot() - ensure.Nil(t, (&newCmd{}).cloneSampleCloudCode(h.env, true)) + ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) h.Out.Reset() - c, err := configFromDir(h.env.Root) + c, err := parsecli.ConfigFromDir(h.Env.Root) ensure.Nil(t, err) - config, ok := (c).(*parseConfig) + config, ok := (c).(*parsecli.ParseConfig) ensure.True(t, ok) - return h, &context{Config: config} + return h, &parsecli.Context{Config: config} } func TestGetAllJSVersions(t *testing.T) { @@ -61,7 +69,7 @@ func TestGetAllJSVersions(t *testing.T) { h := newJsSdkHarness(t) defer h.Stop() j := jsSDKCmd{} - versions, err := j.getAllJSSdks(h.env) + versions, err := j.getAllJSSdks(h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, versions, []string{"1.2.11", "1.2.10", "1.2.9", "1.2.8", "0.2.0"}) } @@ -71,7 +79,7 @@ func TestGetAllJSVersionsError(t *testing.T) { h := newJsSdkHarnessError(t) defer h.Stop() j := jsSDKCmd{} - _, err := j.getAllJSSdks(h.env) + _, err := j.getAllJSSdks(h.Env) ensure.Err(t, err, regexp.MustCompile(`something is wrong`)) } @@ -80,7 +88,7 @@ func TestPrintVersions(t *testing.T) { h, c := newJsSdkHarnessWithConfig(t) defer h.Stop() j := jsSDKCmd{} - ensure.Nil(t, j.printVersions(h.env, c)) + ensure.Nil(t, j.printVersions(h.Env, c)) ensure.DeepEqual(t, h.Out.String(), ` 1.2.11 1.2.10 @@ -95,24 +103,24 @@ func TestSetVersionInvalid(t *testing.T) { h, c := newJsSdkHarnessWithConfig(t) defer h.Stop() j := jsSDKCmd{newVersion: "1.2.12"} - ensure.Err(t, j.setVersion(h.env, c), regexp.MustCompile("Invalid SDK version selected")) + ensure.Err(t, j.setVersion(h.Env, c), regexp.MustCompile("Invalid SDK version selected")) } func TestSetVersionNoneSelected(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - c := &context{Config: defaultParseConfig} + c := &parsecli.Context{Config: defaultParseConfig} var j jsSDKCmd - c.Config.getProjectConfig().Parse.JSSDK = "1.2.1" - ensure.Nil(t, j.getVersion(h.env, c)) + c.Config.GetProjectConfig().Parse.JSSDK = "1.2.1" + ensure.Nil(t, j.getVersion(h.Env, c)) ensure.DeepEqual(t, "Current JavaScript SDK version is 1.2.1\n", h.Out.String()) - c.Config.getProjectConfig().Parse.JSSDK = "" - ensure.Err(t, j.getVersion(h.env, c), + c.Config.GetProjectConfig().Parse.JSSDK = "" + ensure.Err(t, j.getVersion(h.Env, c), regexp.MustCompile("JavaScript SDK version not set for this project.")) } @@ -122,10 +130,10 @@ func TestSetValidVersion(t *testing.T) { h, c := newJsSdkHarnessWithConfig(t) defer h.Stop() j := jsSDKCmd{newVersion: "1.2.11"} - ensure.Nil(t, j.setVersion(h.env, c)) + ensure.Nil(t, j.setVersion(h.Env, c)) ensure.DeepEqual(t, h.Out.String(), "Current JavaScript SDK version is 1.2.11\n") - content, err := ioutil.ReadFile(filepath.Join(h.env.Root, parseProject)) + content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.ParseProject)) ensure.Nil(t, err) ensure.DeepEqual(t, string(content), `{ "project_type": 1, @@ -136,12 +144,12 @@ func TestSetValidVersion(t *testing.T) { } // NOTE: testing for legacy config format -func newLegacyJsSdkHarnessWithConfig(t testing.TB) (*Harness, *context) { +func newLegacyJsSdkHarnessWithConfig(t testing.TB) (*parsecli.Harness, *parsecli.Context) { h := newJsSdkHarness(t) - h.makeEmptyRoot() + h.MakeEmptyRoot() - ensure.Nil(t, os.Mkdir(filepath.Join(h.env.Root, configDir), 0755)) - path := filepath.Join(h.env.Root, legacyConfigFile) + ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) + path := filepath.Join(h.Env.Root, parsecli.LegacyConfigFile) ensure.Nil(t, ioutil.WriteFile(path, []byte(`{ "global": { @@ -150,13 +158,13 @@ func newLegacyJsSdkHarnessWithConfig(t testing.TB) (*Harness, *context) { }`), 0600)) - c, err := configFromDir(h.env.Root) + c, err := parsecli.ConfigFromDir(h.Env.Root) ensure.Nil(t, err) - config, ok := (c).(*parseConfig) + config, ok := (c).(*parsecli.ParseConfig) ensure.True(t, ok) - return h, &context{Config: config} + return h, &parsecli.Context{Config: config} } func TestLegacySetValidVersion(t *testing.T) { @@ -165,6 +173,6 @@ func TestLegacySetValidVersion(t *testing.T) { h, c := newLegacyJsSdkHarnessWithConfig(t) defer h.Stop() j := jsSDKCmd{newVersion: "1.2.11"} - ensure.Nil(t, j.setVersion(h.env, c)) + ensure.Nil(t, j.setVersion(h.Env, c)) ensure.DeepEqual(t, h.Out.String(), "Current JavaScript SDK version is 1.2.11\n") } diff --git a/logs_cmd.go b/parsecmd/logs.go similarity index 86% rename from logs_cmd.go rename to parsecmd/logs.go index 7a7b13b..2cc19bd 100644 --- a/logs_cmd.go +++ b/parsecmd/logs.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "encoding/json" @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -29,7 +30,7 @@ type logsCmd struct { level string } -func (l *logsCmd) run(e *env, c *context) error { +func (l *logsCmd) run(e *parsecli.Env, c *parsecli.Context) error { level := strings.ToUpper(l.level) if level != "INFO" && level != "ERROR" { return stackerr.Newf("invalid level: %q", l.level) @@ -64,7 +65,7 @@ func (l *logsCmd) run(e *env, c *context) error { return nil } -func (l *logsCmd) round(e *env, c *context, startTime *parseTime) (*parseTime, error) { +func (l *logsCmd) round(e *parsecli.Env, c *parsecli.Context, startTime *parseTime) (*parseTime, error) { v := make(url.Values) v.Set("n", fmt.Sprint(l.num)) v.Set("level", l.level) @@ -95,7 +96,7 @@ func (l *logsCmd) round(e *env, c *context, startTime *parseTime) (*parseTime, e return startTime, nil } -func newLogsCmd(e *env) *cobra.Command { +func NewLogsCmd(e *parsecli.Env) *cobra.Command { l := logsCmd{ level: "INFO", } @@ -103,7 +104,7 @@ func newLogsCmd(e *env) *cobra.Command { Use: "logs", Short: "Prints out recent log messages", Long: "Prints out recent log messages.", - Run: runWithClient(e, l.run), + Run: parsecli.RunWithClient(e, l.run), Aliases: []string{"log"}, } cmd.Flags().UintVarP(&l.num, "num", "n", l.num, diff --git a/logs_cmd_test.go b/parsecmd/logs_test.go similarity index 82% rename from logs_cmd_test.go rename to parsecmd/logs_test.go index e1a63c2..a54bea4 100644 --- a/logs_cmd_test.go +++ b/parsecmd/logs_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "io/ioutil" @@ -8,6 +8,7 @@ import ( "sync/atomic" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" ) @@ -15,18 +16,18 @@ import ( func TestLogInvalidLevel(t *testing.T) { t.Parallel() l := logsCmd{} - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - err := l.run(h.env, nil) + err := l.run(h.Env, nil) ensure.Err(t, err, regexp.MustCompile(`invalid level: ""`)) } func TestLogWithoutFollow(t *testing.T) { t.Parallel() l := logsCmd{level: "INFO"} - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/scriptlog") rows := []logResponse{{Message: "foo bar"}, {Message: "baz"}} return &http.Response{ @@ -34,8 +35,8 @@ func TestLogWithoutFollow(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - err := l.run(h.env, &context{}) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + err := l.run(h.Env, &parsecli.Context{}) ensure.Nil(t, err) ensure.DeepEqual(t, h.Out.String(), "baz\nfoo bar\n") } @@ -43,14 +44,14 @@ func TestLogWithoutFollow(t *testing.T) { func TestLogWithFollow(t *testing.T) { t.Parallel() l := logsCmd{level: "INFO", follow: true} - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var round int64 round1Time := parseTime{ISO: "iso1", Type: "type1"} round1TimeStr := jsonStr(t, round1Time) round3Time := parseTime{ISO: "iso2", Type: "type2"} round3TimeStr := jsonStr(t, round3Time) - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { switch atomic.AddInt64(&round, 1) { case 1: // expect no timestamp, return some data @@ -100,8 +101,8 @@ func TestLogWithFollow(t *testing.T) { } }() - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - err := l.run(h.env, &context{}) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + err := l.run(h.Env, &parsecli.Context{}) close(stop) ensure.Err(t, err, regexp.MustCompile(`parse: error with status=500 and body="a"`)) diff --git a/migrate_cmd.go b/parsecmd/migrate.go similarity index 65% rename from migrate_cmd.go rename to parsecmd/migrate.go index a322d99..468a39b 100644 --- a/migrate_cmd.go +++ b/parsecmd/migrate.go @@ -1,10 +1,11 @@ -package main +package parsecmd import ( "fmt" "os" "path/filepath" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -13,13 +14,13 @@ type migrateCmd struct { retainMaster bool } -func (m *migrateCmd) upgradeLegacy(e *env, c config) (*parseConfig, error) { - p := c.getProjectConfig() - if p.Type != legacyParseFormat { +func (m *migrateCmd) upgradeLegacy(e *parsecli.Env, c parsecli.Config) (*parsecli.ParseConfig, error) { + p := c.GetProjectConfig() + if p.Type != parsecli.LegacyParseFormat { return nil, stackerr.New("Already using the preferred config format.") } - p.Type = parseFormat - config, ok := (c).(*parseConfig) + p.Type = parsecli.ParseFormat + config, ok := (c).(*parsecli.ParseConfig) if !ok { return nil, stackerr.Newf("Unexpected config format: %d", p.Type) } @@ -31,8 +32,8 @@ func (m *migrateCmd) upgradeLegacy(e *env, c config) (*parseConfig, error) { return config, nil } -func (m *migrateCmd) run(e *env) error { - c, err := configFromDir(e.Root) +func (m *migrateCmd) run(e *parsecli.Env) error { + c, err := parsecli.ConfigFromDir(e.Root) if err != nil { return err } @@ -41,21 +42,21 @@ func (m *migrateCmd) run(e *env) error { if err != nil { return err } - localErr := storeConfig(e, c) - projectErr := storeProjectConfig(e, c) + localErr := parsecli.StoreConfig(e, c) + projectErr := parsecli.StoreProjectConfig(e, c) if localErr == nil && projectErr == nil { - legacy := filepath.Join(e.Root, legacyConfigFile) + legacy := filepath.Join(e.Root, parsecli.LegacyConfigFile) err := os.Remove(legacy) if err != nil { fmt.Fprintf(e.Err, "Could not delete: %q. Please remove this file manually.\n", legacy) } } else { - local := filepath.Join(e.Root, parseLocal) + local := filepath.Join(e.Root, parsecli.ParseLocal) err := os.Remove(local) if err != nil { fmt.Fprintf(e.Err, "Failed to clean up: %q. Please remove this file manually.\n", local) } - project := filepath.Join(e.Root, parseProject) + project := filepath.Join(e.Root, parsecli.ParseProject) err = os.Remove(project) if err != nil { fmt.Fprintf(e.Err, "Failed to clean up: %q. Please remove this file manually.\n", project) @@ -65,7 +66,7 @@ func (m *migrateCmd) run(e *env) error { return nil } -func newMigrateCmd(e *env) *cobra.Command { +func NewMigrateCmd(e *parsecli.Env) *cobra.Command { var m migrateCmd cmd := &cobra.Command{ Use: "migrate", @@ -73,7 +74,7 @@ func newMigrateCmd(e *env) *cobra.Command { Long: `Use this on projects with legacy config format to migrate to the preferred format. `, - Run: runNoArgs(e, m.run), + Run: parsecli.RunNoArgs(e, m.run), } cmd.Flags().BoolVarP(&m.retainMaster, "retain", "r", m.retainMaster, "Retain any master keys present in config during migration") diff --git a/migrate_cmd_test.go b/parsecmd/migrate_test.go similarity index 52% rename from migrate_cmd_test.go rename to parsecmd/migrate_test.go index 77fd429..c71846d 100644 --- a/migrate_cmd_test.go +++ b/parsecmd/migrate_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "io/ioutil" @@ -8,35 +8,36 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) func TestUpgradeLegacyNoOp(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var m migrateCmd - c := &parseConfig{projectConfig: &projectConfig{Type: parseFormat}} - _, err := m.upgradeLegacy(h.env, c) + c := &parsecli.ParseConfig{ProjectConfig: &parsecli.ProjectConfig{Type: parsecli.ParseFormat}} + _, err := m.upgradeLegacy(h.Env, c) ensure.Err(t, err, regexp.MustCompile("Already using the preferred config format.")) } func TestUpgradeLegacyRetainMaster(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() m := migrateCmd{retainMaster: true} - c := &parseConfig{ - Applications: map[string]*parseAppConfig{ + c := &parsecli.ParseConfig{ + Applications: map[string]*parsecli.ParseAppConfig{ "app": {ApplicationID: "a", MasterKey: "m"}, }, - projectConfig: &projectConfig{Type: legacyParseFormat}, + ProjectConfig: &parsecli.ProjectConfig{Type: parsecli.LegacyParseFormat}, } - config, err := m.upgradeLegacy(h.env, c) + config, err := m.upgradeLegacy(h.Env, c) ensure.Nil(t, err) ensure.DeepEqual(t, config.Applications["app"].MasterKey, "m") } @@ -44,17 +45,17 @@ func TestUpgradeLegacyRetainMaster(t *testing.T) { func TestUpgradeLegacy(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() m := migrateCmd{retainMaster: false} - c := &parseConfig{ - Applications: map[string]*parseAppConfig{ + c := &parsecli.ParseConfig{ + Applications: map[string]*parsecli.ParseAppConfig{ "app": {ApplicationID: "a", MasterKey: "m"}, }, - projectConfig: &projectConfig{Type: legacyParseFormat}, + ProjectConfig: &parsecli.ProjectConfig{Type: parsecli.LegacyParseFormat}, } - config, err := m.upgradeLegacy(h.env, c) + config, err := m.upgradeLegacy(h.Env, c) ensure.Nil(t, err) ensure.DeepEqual(t, config.Applications["app"].MasterKey, "") } @@ -62,38 +63,38 @@ func TestUpgradeLegacy(t *testing.T) { func TestUpgradeLegacyWithEmail(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() m := migrateCmd{retainMaster: false} - c := &parseConfig{ - Applications: map[string]*parseAppConfig{ + c := &parsecli.ParseConfig{ + Applications: map[string]*parsecli.ParseAppConfig{ "app": {ApplicationID: "a", MasterKey: "m"}, }, - projectConfig: &projectConfig{ - Type: legacyParseFormat, + ProjectConfig: &parsecli.ProjectConfig{ + Type: parsecli.LegacyParseFormat, ParserEmail: "test@email.com", }, } - config, err := m.upgradeLegacy(h.env, c) + config, err := m.upgradeLegacy(h.Env, c) ensure.Nil(t, err) ensure.DeepEqual(t, config.Applications["app"].MasterKey, "") - ensure.DeepEqual(t, config.getProjectConfig().ParserEmail, "test@email.com") + ensure.DeepEqual(t, config.GetProjectConfig().ParserEmail, "test@email.com") } func TestRunMigrateCmd(t *testing.T) { t.Parallel() - h := newHarness(t) - h.makeEmptyRoot() + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() defer h.Stop() n := &newCmd{} - h.env.Type = parseFormat - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - _, err := n.setupSample(h.env, + h.Env.Type = parsecli.ParseFormat + h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) + _, err := n.setupSample(h.Env, "yolo", - &parseAppConfig{ + &parsecli.ParseAppConfig{ ApplicationID: "yolo-id", MasterKey: "yoda", }, @@ -103,16 +104,16 @@ func TestRunMigrateCmd(t *testing.T) { ensure.Nil(t, err) m := &migrateCmd{} - err = m.run(h.env) + err = m.run(h.Env) ensure.Err(t, err, regexp.MustCompile("Already using the preferred config format")) - ensure.Nil(t, os.Remove(filepath.Join(h.env.Root, parseLocal))) - ensure.Nil(t, os.Remove(filepath.Join(h.env.Root, parseProject))) + ensure.Nil(t, os.Remove(filepath.Join(h.Env.Root, parsecli.ParseLocal))) + ensure.Nil(t, os.Remove(filepath.Join(h.Env.Root, parsecli.ParseProject))) - ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, configDir), 0755)) + ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) ensure.Nil(t, ioutil.WriteFile( - filepath.Join(h.env.Root, legacyConfigFile), + filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), []byte(`{ "applications": { "yolo": { @@ -125,6 +126,6 @@ func TestRunMigrateCmd(t *testing.T) { ), ) - ensure.Nil(t, m.run(h.env)) + ensure.Nil(t, m.run(h.Env)) ensure.StringContains(t, h.Out.String(), "Successfully migrated") } diff --git a/new.go b/parsecmd/new_common.go similarity index 81% rename from new.go rename to parsecmd/new_common.go index 544c1e5..aabda08 100644 --- a/new.go +++ b/parsecmd/new_common.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "fmt" @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/parse" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" @@ -21,14 +22,7 @@ type newCmd struct { codeLocation string // location of cloud code project } -var newProjectFiles = []struct { - dirname, filename, content string -}{ - {"cloud", "main.js", sampleSource}, - {"public", "index.html", sampleHTML}, -} - -func (n *newCmd) curlCommand(app *app) string { +func (n *newCmd) curlCommand(app *parsecli.App) string { return fmt.Sprintf( `curl -X POST \ -H "X-Parse-Application-Id: %s" \ @@ -42,7 +36,7 @@ func (n *newCmd) curlCommand(app *app) string { ) } -func (n *newCmd) cloudCodeHelpMessage(e *env, app *app) string { +func (n *newCmd) cloudCodeHelpMessage(e *parsecli.Env, app *parsecli.App) string { return fmt.Sprintf( `Your Cloud Code has been created at %s. Next, you might want to deploy this code with "parse deploy". @@ -56,7 +50,7 @@ you can test that it works, with: ) } -func (n *newCmd) getCloudCodeDir(e *env, appName string, isNew bool) (string, error) { +func (n *newCmd) getCloudCodeDir(e *parsecli.Env, appName string, isNew bool) (string, error) { var cloudCodeDir string if isNew { fmt.Fprintf(e.Out, @@ -91,7 +85,7 @@ Directory Name: `, and select the app you just created.` } - configFile := filepath.Join(e.Root, cloudCodeDir, legacyConfigFile) + configFile := filepath.Join(e.Root, cloudCodeDir, parsecli.LegacyConfigFile) if _, err := os.Lstat(configFile); err == nil { return "", stackerr.Newf( `Sorry, we are unable to create Cloud Code at %s. @@ -131,26 +125,7 @@ so it does not conflict with any other Cloud Code in the current directory. return cloudCodeDir, nil } -func (n *newCmd) createConfigWithContent(path, content string) error { - file, err := os.OpenFile( - path, - os.O_RDWR|os.O_CREATE|os.O_TRUNC, - 0600, - ) - if err != nil && !os.IsExist(err) { - return stackerr.Wrap(err) - } - defer file.Close() - if _, err := file.WriteString(content); err != nil { - return stackerr.Wrap(err) - } - if err := file.Close(); err != nil { - return stackerr.Wrap(err) - } - return nil -} - -func (n *newCmd) promptCreateNewApp(e *env, nonInteractive bool) (string, error) { +func (n *newCmd) promptCreateNewApp(e *parsecli.Env, nonInteractive bool) (string, error) { if nonInteractive || n.noCode { if n.createNewApp { return "new", nil @@ -179,16 +154,16 @@ Type "(n)ew" or "(e)xisting": `, } func (n *newCmd) setupSample( - e *env, + e *parsecli.Env, name string, - appConfig appConfig, + appConfig parsecli.AppConfig, isNew bool, nonInteractive bool, ) (bool, error) { - found := isProjectDir(getProjectRoot(e, e.Root)) + found := parsecli.IsProjectDir(parsecli.GetProjectRoot(e, e.Root)) if !found { - root := getLegacyProjectRoot(e, e.Root) - _, err := os.Lstat(filepath.Join(root, legacyConfigFile)) + root := parsecli.GetLegacyProjectRoot(e, e.Root) + _, err := os.Lstat(filepath.Join(root, parsecli.LegacyConfigFile)) found = err == nil } if found { @@ -217,7 +192,7 @@ Please refrain from creating a Parse project inside another Parse project. e.Root = filepath.Join(e.Root, cloudCodeDir) switch e.Type { - case parseFormat: + case parsecli.ParseFormat: if !n.configOnly { var decision string if isNew { @@ -238,13 +213,13 @@ Please type [y] if you wish to download the current Cloud Code or [n] for blank dumpTemplate := false if !isNew && !n.configOnly { // if parse app was already created try to fetch cloud code and populate dir - masterKey, err := appConfig.getMasterKey(e) + masterKey, err := appConfig.GetMasterKey(e) if err != nil { return false, err } e.ParseAPIClient = e.ParseAPIClient.WithCredentials( parse.MasterKey{ - ApplicationID: appConfig.getApplicationID(), + ApplicationID: appConfig.GetApplicationID(), MasterKey: masterKey, }, ) @@ -267,7 +242,7 @@ This will download Cloud Code to a temporary location. } } dumpTemplate = (isNew || dumpTemplate) && !n.configOnly - return dumpTemplate, n.cloneSampleCloudCode(e, dumpTemplate) + return dumpTemplate, parsecli.CloneSampleCloudCode(e, dumpTemplate) } return false, stackerr.Newf("Unknown project type: %d", e.Type) } @@ -275,40 +250,40 @@ This will download Cloud Code to a temporary location. func (n *newCmd) configureSample( add *addCmd, name string, - appConfig appConfig, + appConfig parsecli.AppConfig, args []string, - e *env, + e *parsecli.Env, ) error { if err := add.addSelectedApp(name, appConfig, args, e); err != nil { return err } - masterKey, err := appConfig.getMasterKey(e) + masterKey, err := appConfig.GetMasterKey(e) if err != nil { return err } e.ParseAPIClient = e.ParseAPIClient.WithCredentials( parse.MasterKey{ - ApplicationID: appConfig.getApplicationID(), + ApplicationID: appConfig.GetApplicationID(), MasterKey: masterKey, }, ) - if e.Type == parseFormat { + if e.Type == parsecli.ParseFormat { return useLatestJSSDK(e) } return nil } -func (n *newCmd) run(e *env) error { - apps := &apps{} +func (n *newCmd) run(e *parsecli.Env) error { + apps := &parsecli.Apps{} addCmd := &addCmd{MakeDefault: true, apps: apps} - if err := apps.login.authUser(e); err != nil { + if err := apps.Login.AuthUser(e); err != nil { return err } var ( - app *app + app *parsecli.App err error ) @@ -328,13 +303,13 @@ func (n *newCmd) run(e *env) error { } // we pass retries so that even in non interactive mode we can create an app, // and failure does not print 3 times to the screen - app, err = apps.createApp(e, n.parseAppName, createRetries) + app, err = apps.CreateApp(e, n.parseAppName, createRetries) if err != nil { return err } if n.noCode { fmt.Fprintln(e.Out, "Successfully created the app.") - return apps.printApp(e, app) + return apps.PrintApp(e, app) } case "existing", "e": app, err = addCmd.selectApp(e, n.parseAppName) @@ -343,11 +318,11 @@ func (n *newCmd) run(e *env) error { } if n.noCode { fmt.Fprintln(e.Out, "Successfully selected the app.") - return apps.printApp(e, app) + return apps.PrintApp(e, app) } } - e.Type = parseFormat + e.Type = parsecli.ParseFormat appConfig := addCmd.getParseAppConfig(app) dumpTemplate, err := n.setupSample(e, app.Name, appConfig, isNew, nonInteractive) @@ -357,12 +332,12 @@ func (n *newCmd) run(e *env) error { if err := n.configureSample(addCmd, app.Name, appConfig, nil, e); err != nil { return err } - if token := apps.login.credentials.token; token != "" { - email, err := apps.login.authToken(e, token) + if token := apps.Login.Credentials.Token; token != "" { + email, err := apps.Login.AuthToken(e, token) if err != nil { return err } - if err := (&configureCmd{}).parserEmail(e, []string{email}); err != nil { + if err := parsecli.SetParserEmail(e, email); err != nil { return err } } @@ -374,7 +349,7 @@ func (n *newCmd) run(e *env) error { return nil } -func newNewCmd(e *env) *cobra.Command { +func NewNewCmd(e *parsecli.Env) *cobra.Command { nc := newCmd{addApplication: true} cmd := &cobra.Command{ Use: "new", @@ -382,7 +357,7 @@ func newNewCmd(e *env) *cobra.Command { Long: `Adds Cloud Code to an existing Parse app, additional can create a new Parse app. You can also use it in non-interactive mode by using the various flags available. `, - Run: runNoArgs(e, nc.run), + Run: parsecli.RunNoArgs(e, nc.run), } cmd.Flags().BoolVarP(&nc.configOnly, "init", "i", nc.configOnly, "Create a Cloud Code project only with configuration.") diff --git a/new_cmd_test.go b/parsecmd/new_test.go similarity index 52% rename from new_cmd_test.go rename to parsecmd/new_test.go index d64390f..110d911 100644 --- a/new_cmd_test.go +++ b/parsecmd/new_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "fmt" @@ -10,23 +10,24 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" ) -func newNewCmdHarness(t testing.TB) (*Harness, *newCmd) { - h := newHarness(t) - h.makeEmptyRoot() +func newNewCmdHarness(t testing.TB) (*parsecli.Harness, *newCmd) { + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() return h, &newCmd{} } func TestCloudCodeHelpMessage(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() n := &newCmd{} - msg := n.cloudCodeHelpMessage(h.env, &app{ApplicationID: "a", MasterKey: "m"}) + msg := n.cloudCodeHelpMessage(h.Env, &parsecli.App{ApplicationID: "a", MasterKey: "m"}) ensure.StringContains(t, msg, fmt.Sprintf( @@ -34,51 +35,51 @@ func TestCloudCodeHelpMessage(t *testing.T) { Next, you might want to deploy this code with "parse deploy". This includes a "Hello world" cloud function, so once you deploy you can test that it works, with:`, - h.env.Root, + h.Env.Root, ), ) } func TestGetCloudCodeDir(t *testing.T) { t.Parallel() - h := newHarness(t) - h.makeEmptyRoot() + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() defer h.Stop() n := &newCmd{} - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - name, err := n.getCloudCodeDir(h.env, "myapp", true) + h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) + name, err := n.getCloudCodeDir(h.Env, "myapp", true) ensure.Nil(t, err) ensure.StringContains(t, h.Out.String(), "Now it's time to set up some Cloud Code") ensure.DeepEqual(t, name, "myapp") h.Out.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("otherApp\n")) - name, err = n.getCloudCodeDir(h.env, "myapp", true) + h.Env.In = ioutil.NopCloser(strings.NewReader("otherApp\n")) + name, err = n.getCloudCodeDir(h.Env, "myapp", true) ensure.Nil(t, err) ensure.StringContains(t, h.Out.String(), "Now it's time to set up some Cloud Code") ensure.DeepEqual(t, name, "otherApp") - _, err = os.Create(filepath.Join(h.env.Root, "otherApp")) + _, err = os.Create(filepath.Join(h.Env.Root, "otherApp")) ensure.Nil(t, err) h.Out.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("otherApp\n")) - name, err = n.getCloudCodeDir(h.env, "myapp", true) + h.Env.In = ioutil.NopCloser(strings.NewReader("otherApp\n")) + name, err = n.getCloudCodeDir(h.Env, "myapp", true) ensure.Err(t, err, regexp.MustCompile("already exists")) ensure.StringContains(t, h.Out.String(), "Now it's time to set up some Cloud Code") - ensure.Nil(t, os.MkdirAll(filepath.Join(h.env.Root, "myapp", "config"), 0755)) - _, err = os.Create(filepath.Join(h.env.Root, "myapp", "config", "global.json")) + ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, "myapp", "config"), 0755)) + _, err = os.Create(filepath.Join(h.Env.Root, "myapp", "config", "global.json")) ensure.Nil(t, err) h.Out.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - name, err = n.getCloudCodeDir(h.env, "myapp", true) + h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) + name, err = n.getCloudCodeDir(h.Env, "myapp", true) ensure.Err(t, err, regexp.MustCompile("you already have Cloud Code")) - ensure.Nil(t, os.Remove(filepath.Join(h.env.Root, "myapp", "config", "global.json"))) + ensure.Nil(t, os.Remove(filepath.Join(h.Env.Root, "myapp", "config", "global.json"))) h.Out.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - name, err = n.getCloudCodeDir(h.env, "myapp", false) + h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) + name, err = n.getCloudCodeDir(h.Env, "myapp", false) ensure.Nil(t, err) ensure.StringContains(t, h.Out.String(), "folder where we can download the latest") } @@ -86,15 +87,15 @@ func TestGetCloudCodeDir(t *testing.T) { func TestNewCmdDirs(t *testing.T) { t.Parallel() - h, n := newNewCmdHarness(t) + h, _ := newNewCmdHarness(t) defer h.Stop() - ensure.Nil(t, n.cloneSampleCloudCode(h.env, true)) + ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) var err error - for _, newProjectFile := range newProjectFiles { - _, err = os.Stat(filepath.Join(h.env.Root, newProjectFile.dirname)) + for _, newProjectFile := range parsecli.NewProjectFiles { + _, err = os.Stat(filepath.Join(h.Env.Root, newProjectFile.Dirname)) ensure.Nil(t, err) } } @@ -102,20 +103,20 @@ func TestNewCmdDirs(t *testing.T) { func TestNewCmdContent(t *testing.T) { t.Parallel() - h, n := newNewCmdHarness(t) + h, _ := newNewCmdHarness(t) defer h.Stop() - ensure.Nil(t, n.cloneSampleCloudCode(h.env, true)) + ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) - for _, newProjectFile := range newProjectFiles { + for _, newProjectFile := range parsecli.NewProjectFiles { content, err := ioutil.ReadFile( - filepath.Join(h.env.Root, newProjectFile.dirname, newProjectFile.filename), + filepath.Join(h.Env.Root, newProjectFile.Dirname, newProjectFile.Filename), ) ensure.Nil(t, err) - ensure.DeepEqual(t, string(content), newProjectFile.content) + ensure.DeepEqual(t, string(content), newProjectFile.Content) } - content, err := ioutil.ReadFile(filepath.Join(h.env.Root, parseProject)) + content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.ParseProject)) ensure.Nil(t, err) ensure.DeepEqual(t, string(content), @@ -124,11 +125,11 @@ func TestNewCmdContent(t *testing.T) { "project_type" : %d, "parse": {"jssdk":""} }`, - parseFormat, + parsecli.ParseFormat, ), ) - content, err = ioutil.ReadFile(filepath.Join(h.env.Root, parseLocal)) + content, err = ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.ParseLocal)) ensure.Nil(t, err) ensure.DeepEqual(t, string(content), "{}") @@ -137,7 +138,7 @@ func TestNewCmdContent(t *testing.T) { func TestCurlCommand(t *testing.T) { t.Parallel() n := &newCmd{} - command := n.curlCommand(&app{ApplicationID: "AppID", RestKey: "RestKey"}) + command := n.curlCommand(&parsecli.App{ApplicationID: "AppID", RestKey: "RestKey"}) ensure.DeepEqual(t, command, `curl -X POST \ @@ -151,71 +152,71 @@ func TestCurlCommand(t *testing.T) { func TestCreateNewAppNoCode(t *testing.T) { t.Parallel() - h, _ := newAppHarness(t) + h, _ := parsecli.NewAppHarness(t) defer h.Stop() - h.env.In = ioutil.NopCloser(strings.NewReader("email\npassword")) + h.Env.In = ioutil.NopCloser(strings.NewReader("email\npassword")) n := &newCmd{noCode: true, createNewApp: true, parseAppName: "yolo"} - ensure.Nil(t, n.run(h.env)) + ensure.Nil(t, n.run(h.Env)) ensure.StringContains(t, h.Out.String(), "Successfully created") } func TestSelectNewAppNoCode(t *testing.T) { t.Parallel() - h, _ := newAppHarness(t) + h, _ := parsecli.NewAppHarness(t) defer h.Stop() - h.env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) + h.Env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) n := &newCmd{noCode: true, parseAppName: "A"} - ensure.Nil(t, n.run(h.env)) + ensure.Nil(t, n.run(h.Env)) ensure.StringContains(t, h.Out.String(), "Successfully selected") } func TestShouldCreateNewApp(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() n := &newCmd{} - h.env.In = ioutil.NopCloser(strings.NewReader("new")) - decision, err := n.promptCreateNewApp(h.env, false) + h.Env.In = ioutil.NopCloser(strings.NewReader("new")) + decision, err := n.promptCreateNewApp(h.Env, false) ensure.Nil(t, err) ensure.DeepEqual(t, decision, "new") - h.env.In = ioutil.NopCloser(strings.NewReader("existing")) - decision, err = n.promptCreateNewApp(h.env, false) + h.Env.In = ioutil.NopCloser(strings.NewReader("existing")) + decision, err = n.promptCreateNewApp(h.Env, false) ensure.Nil(t, err) ensure.DeepEqual(t, decision, "existing") - h.env.In = ioutil.NopCloser(strings.NewReader("other")) - _, err = n.promptCreateNewApp(h.env, false) + h.Env.In = ioutil.NopCloser(strings.NewReader("other")) + _, err = n.promptCreateNewApp(h.Env, false) ensure.Err(t, err, regexp.MustCompile("are the only valid options")) - decision, err = n.promptCreateNewApp(h.env, true) + decision, err = n.promptCreateNewApp(h.Env, true) ensure.Nil(t, err) ensure.DeepEqual(t, decision, "existing") n.createNewApp = true - decision, err = n.promptCreateNewApp(h.env, true) + decision, err = n.promptCreateNewApp(h.Env, true) ensure.Nil(t, err) ensure.DeepEqual(t, decision, "new") } func TestSetupAndConfigure(t *testing.T) { t.Parallel() - h := newHarness(t) - h.makeEmptyRoot() + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() defer h.Stop() n := &newCmd{} - h.env.Type = parseFormat - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - code, err := n.setupSample(h.env, "myapp", &parseAppConfig{ApplicationID: "a"}, true, false) + h.Env.Type = parsecli.ParseFormat + h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) + code, err := n.setupSample(h.Env, "myapp", &parsecli.ParseAppConfig{ApplicationID: "a"}, true, false) ensure.Nil(t, err) ensure.True(t, code) - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/jsVersions") rows := jsSDKVersion{JS: []string{"1.5.0", "1.6.0"}} return &http.Response{ @@ -223,18 +224,13 @@ func TestSetupAndConfigure(t *testing.T) { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} err = n.configureSample( &addCmd{MakeDefault: true}, "yolo", - &parseAppConfig{ApplicationID: "a", masterKey: "m"}, + (&parsecli.ParseAppConfig{ApplicationID: "a"}).WithInternalMasterKey("m"), nil, - h.env, + h.Env, ) ensure.Nil(t, err) - - d := &defaultCmd{} - h.Out.Reset() - ensure.Nil(t, d.run(h.env, nil)) - ensure.DeepEqual(t, h.Out.String(), "Current default app is yolo\n") } diff --git a/parse_symbol_uploader.go b/parsecmd/parse_symbol_uploader.go similarity index 93% rename from parse_symbol_uploader.go rename to parsecmd/parse_symbol_uploader.go index bfdfd0c..f89b7dc 100644 --- a/parse_symbol_uploader.go +++ b/parsecmd/parse_symbol_uploader.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "bufio" @@ -12,6 +12,7 @@ import ( "path" "path/filepath" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/errgroup" "github.com/facebookgo/stackerr" ) @@ -33,9 +34,9 @@ func base64MD5OfFile(filename string) (string, error) { return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil } -func uploadSymbolFiles(files []string, commonHeaders map[string]string, removeFiles bool, e *env) error { +func uploadSymbolFiles(files []string, commonHeaders map[string]string, removeFiles bool, e *parsecli.Env) error { var wg errgroup.Group - uploadFile := func(filename string, e *env) { + uploadFile := func(filename string, e *parsecli.Env) { defer wg.Done() name := filepath.Base(filepath.Clean(filename)) file, err := os.Open(filename) diff --git a/parse_symbol_uploader_test.go b/parsecmd/parse_symbol_uploader_test.go similarity index 72% rename from parse_symbol_uploader_test.go rename to parsecmd/parse_symbol_uploader_test.go index d353cc4..2a2e5ff 100644 --- a/parse_symbol_uploader_test.go +++ b/parsecmd/parse_symbol_uploader_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "fmt" @@ -10,24 +10,25 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" ) -func createRandomFiles(t *testing.T, h *Harness) { +func createRandomFiles(t *testing.T, h *parsecli.Harness) { for _, filename := range []string{"a", "b"} { - ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.env.Root, filename), + ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.Env.Root, filename), []byte(fmt.Sprintf(`Content of file : %s`, filename)), 0755)) } } func TestBase64MD5OfFile(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.makeEmptyRoot() - testFile := filepath.Join(h.env.Root, "test") + h.MakeEmptyRoot() + testFile := filepath.Join(h.Env.Root, "test") ensure.Nil(t, ioutil.WriteFile(testFile, []byte(`A test string`), 0755)) @@ -38,42 +39,42 @@ func TestBase64MD5OfFile(t *testing.T) { func TestBase64MD5OfFileNoFile(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.makeEmptyRoot() - testFile := filepath.Join(h.env.Root, "test") + h.MakeEmptyRoot() + testFile := filepath.Join(h.Env.Root, "test") _, err := base64MD5OfFile(testFile) ensure.NotNil(t, err) } func TestReadDirNames(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.makeEmptyRoot() + h.MakeEmptyRoot() createRandomFiles(t, h) - files, err := readDirNames(h.env.Root) + files, err := readDirNames(h.Env.Root) sort.Strings(files) ensure.DeepEqual(t, len(files), 2) ensure.DeepEqual(t, files[:], []string{ - filepath.Join(h.env.Root, "a"), - filepath.Join(h.env.Root, "b"), + filepath.Join(h.Env.Root, "a"), + filepath.Join(h.Env.Root, "b"), }) ensure.Nil(t, err) } func TestUploadFiles(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.makeEmptyRoot() + h.MakeEmptyRoot() createRandomFiles(t, h) names := []string{"a", "b"} - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { switch filepath.Base(r.URL.Path) { case names[0]: ensure.NotNil(t, r.Header) @@ -100,14 +101,14 @@ func TestUploadFiles(t *testing.T) { }, nil } }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} var filenames []string for _, name := range names { - filenames = append(filenames, filepath.Join(h.env.Root, name)) + filenames = append(filenames, filepath.Join(h.Env.Root, name)) } ensure.Nil(t, uploadSymbolFiles(filenames[:], - map[string]string{"Key": "Value"}, true, h.env)) + map[string]string{"Key": "Value"}, true, h.Env)) for _, filename := range filenames { _, err := os.Lstat(filename) ensure.NotNil(t, err) diff --git a/parseignore.go b/parsecmd/parseignore.go similarity index 92% rename from parseignore.go rename to parsecmd/parseignore.go index 89cc21b..f0d3699 100644 --- a/parseignore.go +++ b/parsecmd/parseignore.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "bytes" @@ -6,16 +6,17 @@ import ( "path/filepath" "strings" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/parseignore" "github.com/facebookgo/symwalk" ) -func ignoreErrors(errors []error, e *env) string { +func ignoreErrors(errors []error, e *parsecli.Env) string { l := len(errors) var b bytes.Buffer for n, err := range errors { - b.WriteString(errorString(e, err)) + b.WriteString(parsecli.ErrorString(e, err)) if n != l-1 { b.WriteString("\n") } diff --git a/parseignore_test.go b/parsecmd/parseignore_test.go similarity index 99% rename from parseignore_test.go rename to parsecmd/parseignore_test.go index acc00c7..7dbe850 100644 --- a/parseignore_test.go +++ b/parsecmd/parseignore_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "fmt" diff --git a/releases_cmd.go b/parsecmd/releases.go similarity index 90% rename from releases_cmd.go rename to parsecmd/releases.go index 2485c5c..393f349 100644 --- a/releases_cmd.go +++ b/parsecmd/releases.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "encoding/json" @@ -8,6 +8,7 @@ import ( "strings" "text/tabwriter" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -30,7 +31,7 @@ type releasesCmd struct { func (r *releasesCmd) printFileNames( fileVersions map[string]interface{}, - e *env) { + e *parsecli.Env) { var files []string for name := range fileVersions { files = append(files, name) @@ -41,7 +42,7 @@ func (r *releasesCmd) printFileNames( func (r *releasesCmd) printFiles(version string, releases []releasesResponse, - e *env) error { + e *parsecli.Env) error { var files string for _, release := range releases { if release.Version == version { @@ -74,7 +75,7 @@ Note that you can list files for all releases shown in "parse releases"`, return nil } -func (r *releasesCmd) run(e *env, c *context) error { +func (r *releasesCmd) run(e *parsecli.Env, c *parsecli.Context) error { u := &url.URL{ Path: "releases", } @@ -101,13 +102,13 @@ func (r *releasesCmd) run(e *env, c *context) error { return nil } -func newReleasesCmd(e *env) *cobra.Command { +func NewReleasesCmd(e *parsecli.Env) *cobra.Command { r := &releasesCmd{} cmd := &cobra.Command{ Use: "releases [app]", Short: "Gets the releases for a Parse App", Long: "Prints the releases the server knows about.", - Run: runWithClient(e, r.run), + Run: parsecli.RunWithClient(e, r.run), } cmd.Flags().StringVarP(&r.version, "version", "v", r.version, "List files names of the deployed version.") diff --git a/releases_cmd_test.go b/parsecmd/releases_test.go similarity index 73% rename from releases_cmd_test.go rename to parsecmd/releases_test.go index b7f0c72..346d2a8 100644 --- a/releases_cmd_test.go +++ b/parsecmd/releases_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "io/ioutil" @@ -7,14 +7,15 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" "github.com/facebookgo/stackerr" ) -func newReleasesCmdHarness(t testing.TB) (*Harness, *releasesCmd) { - h := newHarness(t) - h.makeEmptyRoot() +func newReleasesCmdHarness(t testing.TB) (*parsecli.Harness, *releasesCmd) { + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() r := &releasesCmd{} return h, r } @@ -26,16 +27,16 @@ func TestReleasesCmd(t *testing.T) { {Version: "v1", Description: "version 1", Timestamp: "time 1"}, {Version: "v2", Description: "version 2", Timestamp: "time 2"}, } - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/releases") return &http.Response{ StatusCode: http.StatusOK, Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} - ensure.Nil(t, r.run(h.env, &context{})) + ensure.Nil(t, r.run(h.Env, &parsecli.Context{})) expected := `Name Description Date v1 version 1 time 1 @@ -47,12 +48,12 @@ v2 version 2 time 2 func TestReleasesCmdError(t *testing.T) { h, c := newReleasesCmdHarness(t) defer h.Stop() - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { return nil, stackerr.New("Throws error") }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} - ensure.NotNil(t, c.run(h.env, &context{})) + ensure.NotNil(t, c.run(h.Env, &parsecli.Context{})) } func TestReleasesCmdPrintVersion(t *testing.T) { @@ -76,11 +77,11 @@ func TestReleasesCmdPrintVersion(t *testing.T) { }`, }, } - err := r.printFiles("", releases, h.env) + err := r.printFiles("", releases, h.Env) ensure.Err(t, err, regexp.MustCompile("Unable to fetch files for release version")) h.Out.Reset() - err = r.printFiles("v1", releases, h.env) + err = r.printFiles("v1", releases, h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, h.Out.String(), `Deployed cloud code files: app.js @@ -89,7 +90,7 @@ views/index.js `) h.Out.Reset() - err = r.printFiles("v2", releases, h.env) + err = r.printFiles("v2", releases, h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, h.Out.String(), `Deployed cloud code files: app.js diff --git a/rollback_cmd.go b/parsecmd/rollback.go similarity index 82% rename from rollback_cmd.go rename to parsecmd/rollback.go index 2c0eabb..df3efb0 100644 --- a/rollback_cmd.go +++ b/parsecmd/rollback.go @@ -1,9 +1,10 @@ -package main +package parsecmd import ( "fmt" "net/url" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -16,7 +17,7 @@ type rollbackInfo struct { ReleaseName string `json:"releaseName,omitEmpty"` } -func (r *rollbackCmd) run(e *env, c *context) error { +func (r *rollbackCmd) run(e *parsecli.Env, c *parsecli.Context) error { var req rollbackInfo message := "previous release" if r.ReleaseName != "" { @@ -35,13 +36,13 @@ func (r *rollbackCmd) run(e *env, c *context) error { return nil } -func newRollbackCmd(e *env) *cobra.Command { +func NewRollbackCmd(e *parsecli.Env) *cobra.Command { r := rollbackCmd{} cmd := &cobra.Command{ Use: "rollback [app]", Short: "Rolls back the version for the given app", Long: "Rolls back the version for the given app.", - Run: runWithClient(e, r.run), + Run: parsecli.RunWithClient(e, r.run), } cmd.Flags().StringVarP(&r.ReleaseName, "release", "r", r.ReleaseName, "Provides an optional release to rollback to. If no release is provided, rolls back to the previous release.") diff --git a/rollback_cmd_test.go b/parsecmd/rollback_test.go similarity index 67% rename from rollback_cmd_test.go rename to parsecmd/rollback_test.go index 766d562..dc4af2a 100644 --- a/rollback_cmd_test.go +++ b/parsecmd/rollback_test.go @@ -1,4 +1,4 @@ -package main +package parsecmd import ( "encoding/json" @@ -8,13 +8,14 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" ) -func newRollbackCmdHarness(t testing.TB) *Harness { - h := newHarness(t) - ht := transportFunc(func(r *http.Request) (*http.Response, error) { +func newRollbackCmdHarness(t testing.TB) *parsecli.Harness { + h := parsecli.NewHarness(t) + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/deploy") var req, res rollbackInfo ensure.Nil(t, json.NewDecoder(r.Body).Decode(&req)) @@ -28,7 +29,7 @@ func newRollbackCmdHarness(t testing.TB) *Harness { Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} return h } @@ -37,7 +38,7 @@ func TestRollbackToPrevious(t *testing.T) { var r rollbackCmd h := newRollbackCmdHarness(t) defer h.Stop() - ensure.Nil(t, r.run(h.env, nil)) + ensure.Nil(t, r.run(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Rolling back to previous release Rolled back to version v0 @@ -49,7 +50,7 @@ func TestRollbackToVersionOne(t *testing.T) { r := rollbackCmd{ReleaseName: "v1"} h := newRollbackCmdHarness(t) defer h.Stop() - ensure.Nil(t, r.run(h.env, nil)) + ensure.Nil(t, r.run(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Rolling back to v1 Rolled back to version v1 @@ -59,17 +60,17 @@ Rolled back to version v1 func TestRollbackError(t *testing.T) { t.Parallel() var r rollbackCmd - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() var res struct{ Error string } res.Error = "something is wrong" - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/deploy") return &http.Response{ StatusCode: http.StatusExpectationFailed, Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - ensure.Err(t, r.run(h.env, nil), regexp.MustCompile("something is wrong")) + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} + ensure.Err(t, r.run(h.Env, nil), regexp.MustCompile("something is wrong")) } diff --git a/symbols_cmd.go b/parsecmd/symbols.go similarity index 87% rename from symbols_cmd.go rename to parsecmd/symbols.go index f5dd469..ccba654 100644 --- a/symbols_cmd.go +++ b/parsecmd/symbols.go @@ -1,8 +1,9 @@ -package main +package parsecmd import ( "fmt" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -15,7 +16,7 @@ type symbolsCmd struct { skipOsCheck bool } -func (s *symbolsCmd) run(e *env, c *context) error { +func (s *symbolsCmd) run(e *parsecli.Env, c *parsecli.Context) error { android := &androidSymbolUploader{ Path: s.path, Apk: s.apk, @@ -45,14 +46,14 @@ func (s *symbolsCmd) run(e *env, c *context) error { } } -func newSymbolsCmd(e *env) *cobra.Command { +func NewSymbolsCmd(e *parsecli.Env) *cobra.Command { var s symbolsCmd cmd := &cobra.Command{ Use: "symbols [app]", Short: "Uploads symbol files", Long: `Uploads the symbol files for the application to symbolicate crash reports with. Path specifies the path to xcarchive/dSYM/DWARF for iOS or mapping.txt for Android.`, - Run: runWithClient(e, s.run), + Run: parsecli.RunWithClient(e, s.run), } cmd.Flags().StringVarP(&s.path, "path", "p", s.path, "Path to symbols files") diff --git a/symbols_cmd_test.go b/parsecmd/symbols_test.go similarity index 52% rename from symbols_cmd_test.go rename to parsecmd/symbols_test.go index 75591a5..dac7389 100644 --- a/symbols_cmd_test.go +++ b/parsecmd/symbols_test.go @@ -1,18 +1,19 @@ -package main +package parsecmd import ( "regexp" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" ) func TestRunSymbolsCmd(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() s := &symbolsCmd{} - ensure.Err(t, s.run(h.env, nil), regexp.MustCompile("Please specify path")) + ensure.Err(t, s.run(h.Env, nil), regexp.MustCompile("Please specify path")) } diff --git a/templates.go b/parsecmd/templates.go similarity index 58% rename from templates.go rename to parsecmd/templates.go index 12d966b..5ff701e 100644 --- a/templates.go +++ b/parsecmd/templates.go @@ -1,37 +1,6 @@ -package main +package parsecmd const ( - sampleSource = ` -// Use Parse.Cloud.define to define as many cloud functions as you want. -// For example: -Parse.Cloud.define("hello", function(request, response) { - response.success("Hello world!"); -}); -` - - sampleHTML = ` - - - My ParseApp site - - - -
-

Congratulations! You're already hosting with Parse.

-

To get started, edit this file at public/index.html and start adding static content.

-

If you want something a bit more dynamic, delete this file and check out our hosting docs.

-
- - -` - sampleAppJS = ` // These two lines are required to initialize Express in Cloud Code. express = require('express'); diff --git a/parsecmd/utils.go b/parsecmd/utils.go new file mode 100644 index 0000000..fce19e9 --- /dev/null +++ b/parsecmd/utils.go @@ -0,0 +1,68 @@ +package parsecmd + +import ( + "regexp" + "strconv" +) + +var numbers = regexp.MustCompile("[0-9]+") + +func numericLessThan(a, b string) bool { + aLen, bLen := len(a), len(b) + minLen := aLen + if bLen < minLen { + minLen = bLen + } + if minLen == 0 { + if aLen == 0 { + return bLen != 0 + } + return false + } + + i := 0 + for ; i < minLen && a[i] == b[i]; i++ { + } + if i == minLen { + if aLen == bLen { + return false + } + if aLen == minLen { + return true + } + return false + } + + a, b = a[i:], b[i:] + aNos, bNos := numbers.FindAllStringIndex(a, 1), numbers.FindAllStringIndex(b, 1) + + if len(aNos) == 1 && aNos[0][0] == 0 && + len(bNos) == 1 && bNos[0][0] == 0 { + aNum, err := strconv.Atoi(a[:aNos[0][1]]) + if err != nil { + return a < b + } + bNum, err := strconv.Atoi(b[:bNos[0][1]]) + if err != nil { + return a < b + } + if aNum != bNum { + return aNum < bNum + } + } + return a < b +} + +type naturalStrings []string + +func (v naturalStrings) Len() int { + return len(v) +} + +func (v naturalStrings) Less(i, j int) bool { + return !numericLessThan(v[i], v[j]) // desc order +} + +func (v naturalStrings) Swap(i, j int) { + v[i], v[j] = v[j], v[i] +} diff --git a/parsecmd/utils_test.go b/parsecmd/utils_test.go new file mode 100644 index 0000000..29e3e1e --- /dev/null +++ b/parsecmd/utils_test.go @@ -0,0 +1,68 @@ +package parsecmd + +import ( + "crypto/rand" + "testing" + + "github.com/facebookgo/ensure" +) + +func TestBothEmpty(t *testing.T) { + t.Parallel() + ensure.False(t, numericLessThan("", "")) +} + +func TestOnlyAEmpty(t *testing.T) { + t.Parallel() + ensure.True(t, numericLessThan("", "b")) +} + +func TestOnlyBEmpty(t *testing.T) { + t.Parallel() + ensure.False(t, numericLessThan("a", "")) +} + +func TestBothEqual(t *testing.T) { + t.Parallel() + ensure.False(t, numericLessThan("abc", "abc")) +} + +func TestAPrefixOfB(t *testing.T) { + t.Parallel() + ensure.True(t, numericLessThan("abc", "abcd")) +} + +func TestBPrefixOfA(t *testing.T) { + t.Parallel() + ensure.False(t, numericLessThan("abcd", "abc")) +} + +func TestABSansNumbers(t *testing.T) { + t.Parallel() + + const max = 10 + dictionary := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + var rb [max]byte + randomString := func(length int) string { + bytes := rb[:length] + _, err := rand.Read(bytes) + ensure.Nil(t, err) + for k, v := range bytes { + bytes[k] = dictionary[v%byte(len(dictionary))] + } + return string(bytes) + } + + for i := 1; i < max; i++ { + for j := 1; j < max; j++ { + a, b := randomString(i), randomString(j) + ensure.DeepEqual(t, numericLessThan(a, b), a < b) + } + } +} + +func TestABWithNumbers(t *testing.T) { + ensure.True(t, numericLessThan("abc9", "abc12")) + ensure.False(t, numericLessThan("abc12", "abc9")) + ensure.DeepEqual(t, numericLessThan("abc001", "abc1"), "abc001" < "abc1") +} diff --git a/triggers_cmd.go b/triggers.go similarity index 85% rename from triggers_cmd.go rename to triggers.go index 0845ba6..d3b3fcb 100644 --- a/triggers_cmd.go +++ b/triggers.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -32,7 +33,7 @@ type triggerHooksCmd struct { interactive bool } -func readTriggerName(e *env, params *triggerHook) (*triggerHook, error) { +func readTriggerName(e *parsecli.Env, params *triggerHook) (*triggerHook, error) { if params != nil && params.ClassName != "" && params.TriggerName != "" { return params, nil } @@ -52,7 +53,7 @@ func readTriggerName(e *env, params *triggerHook) (*triggerHook, error) { return &t, nil } -func readTriggerParams(e *env, params *triggerHook) (*triggerHook, error) { +func readTriggerParams(e *parsecli.Env, params *triggerHook) (*triggerHook, error) { if params != nil && params.ClassName != "" && params.TriggerName != "" && params.URL != "" { return params, nil } @@ -72,7 +73,7 @@ func readTriggerParams(e *env, params *triggerHook) (*triggerHook, error) { const defaultTriggersURL = "/1/hooks/triggers" -func (h *triggerHooksCmd) triggerHooksCreate(e *env, ctx *context) error { +func (h *triggerHooksCmd) triggerHooksCreate(e *parsecli.Env, ctx *parsecli.Context) error { params, err := readTriggerParams(e, h.Trigger) if err != nil { return err @@ -98,7 +99,7 @@ func (h *triggerHooksCmd) triggerHooksCreate(e *env, ctx *context) error { return nil } -func (h *triggerHooksCmd) triggerHooksRead(e *env, ctx *context) error { +func (h *triggerHooksCmd) triggerHooksRead(e *parsecli.Env, ctx *parsecli.Context) error { u := defaultTriggersURL var trigger *triggerHook if !h.All { @@ -140,7 +141,7 @@ func (h *triggerHooksCmd) triggerHooksRead(e *env, ctx *context) error { return nil } -func (h *triggerHooksCmd) triggerHooksUpdate(e *env, ctx *context) error { +func (h *triggerHooksCmd) triggerHooksUpdate(e *parsecli.Env, ctx *parsecli.Context) error { params, err := readTriggerParams(e, h.Trigger) if err != nil { return err @@ -167,7 +168,7 @@ func (h *triggerHooksCmd) triggerHooksUpdate(e *env, ctx *context) error { return nil } -func (h *triggerHooksCmd) triggerHooksDelete(e *env, ctx *context) error { +func (h *triggerHooksCmd) triggerHooksDelete(e *parsecli.Env, ctx *parsecli.Context) error { params, err := readTriggerName(e, h.Trigger) if err != nil { return err @@ -202,27 +203,27 @@ func (h *triggerHooksCmd) triggerHooksDelete(e *env, ctx *context) error { return nil } -func (h *triggerHooksCmd) triggerHooks(e *env, c *context) error { +func (h *triggerHooksCmd) triggerHooks(e *parsecli.Env, c *parsecli.Context) error { hp := *h hp.All = true return hp.triggerHooksRead(e, c) } -func newTriggerHooksCmd(e *env) *cobra.Command { +func NewTriggerHooksCmd(e *parsecli.Env) *cobra.Command { h := &triggerHooksCmd{interactive: true} c := &cobra.Command{ Use: "triggers", Short: "List cloud code triggers and trigger webhooks", Long: "List cloud code triggers and trigger webhooks", - Run: runWithClient(e, h.triggerHooks), + Run: parsecli.RunWithClient(e, h.triggerHooks), } createCmd := &cobra.Command{ Use: "create", Short: "Create a trigger webhook", Long: "Create a trigger webhook", - Run: runWithClient(e, h.triggerHooksCreate), + Run: parsecli.RunWithClient(e, h.triggerHooksCreate), } c.AddCommand(createCmd) @@ -230,7 +231,7 @@ func newTriggerHooksCmd(e *env) *cobra.Command { Use: "edit", Short: "Edit the URL of a trigger webhook", Long: "Edit the URL of a trigger webhook", - Run: runWithClient(e, h.triggerHooksUpdate), + Run: parsecli.RunWithClient(e, h.triggerHooksUpdate), } c.AddCommand(changeCmd) @@ -238,7 +239,7 @@ func newTriggerHooksCmd(e *env) *cobra.Command { Use: "delete", Short: "Delete a trigger webhook", Long: "Delete a trigger webhook", - Run: runWithClient(e, h.triggerHooksDelete), + Run: parsecli.RunWithClient(e, h.triggerHooksDelete), } c.AddCommand(deleteCmd) diff --git a/triggers_cmd_test.go b/triggers_test.go similarity index 83% rename from triggers_cmd_test.go rename to triggers_test.go index dca461d..982476c 100644 --- a/triggers_cmd_test.go +++ b/triggers_test.go @@ -10,16 +10,17 @@ import ( "strings" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/jsonpipe" "github.com/facebookgo/parse" ) -func newTriggersHarness(t testing.TB) *Harness { - h := newHarness(t) +func newTriggersHarness(t testing.TB) *parsecli.Harness { + h := parsecli.NewHarness(t) defer h.Stop() - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { var body interface{} var params map[string]interface{} if r.Body != nil { @@ -102,7 +103,7 @@ func newTriggersHarness(t testing.TB) *Harness { Body: ioutil.NopCloser(jsonpipe.Encode(body)), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} return h } @@ -120,24 +121,24 @@ func TestTriggerHookString(t *testing.T) { func TestReadTriggerParams(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - h.env.In = ioutil.NopCloser(strings.NewReader("\n")) - _, err := readTriggerName(h.env, nil) + h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) + _, err := readTriggerName(h.Env, nil) ensure.Err(t, err, regexp.MustCompile("Class name cannot be empty")) - h.env.In = ioutil.NopCloser(strings.NewReader("foo\n")) - _, err = readTriggerName(h.env, nil) + h.Env.In = ioutil.NopCloser(strings.NewReader("foo\n")) + _, err = readTriggerName(h.Env, nil) ensure.Err(t, err, regexp.MustCompile("Trigger name cannot be empty")) - h.env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave")) - hook, err := readTriggerName(h.env, nil) + h.Env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave")) + hook, err := readTriggerName(h.Env, nil) ensure.Nil(t, err) ensure.DeepEqual(t, *hook, triggerHook{ClassName: "foo", TriggerName: "beforeSave"}) - h.env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\napi.example.com/foo/beforeSave\n")) - hook, err = readTriggerParams(h.env, nil) + h.Env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\napi.example.com/foo/beforeSave\n")) + hook, err = readTriggerParams(h.Env, nil) ensure.Nil(t, err) ensure.DeepEqual(t, *hook, triggerHook{ ClassName: "foo", @@ -152,7 +153,7 @@ func TestTriggerHooksRead(t *testing.T) { h := newTriggersHarness(t) tr := triggerHooksCmd{All: true} - ensure.Nil(t, tr.triggerHooksRead(h.env, nil)) + ensure.Nil(t, tr.triggerHooksRead(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `The following cloudcode or webhook triggers are associated with this app: Class name: "bar", Trigger name: "afterSave", URL: "https://api.example.com/bar/afterSave" Class name: "foo", Trigger name: "beforeSave" @@ -160,9 +161,9 @@ Class name: "foo", Trigger name: "beforeSave", URL: "https://api.example.com/foo `) tr.All = false - h.env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\n")) + h.Env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\n")) h.Out.Reset() - ensure.Nil(t, tr.triggerHooksRead(h.env, nil)) + ensure.Nil(t, tr.triggerHooksRead(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter following details about the trigger webhook Class name: Trigger name: The following triggers named: "beforeSave" are associated with the class: "foo" Class name: "foo", Trigger name: "beforeSave" @@ -176,8 +177,8 @@ func TestTriggerHooksCreate(t *testing.T) { h := newTriggersHarness(t) var tr triggerHooksCmd - h.env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\napi.example.com/foo/beforeSave\n")) - ensure.Nil(t, tr.triggerHooksCreate(h.env, nil)) + h.Env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\napi.example.com/foo/beforeSave\n")) + ensure.Nil(t, tr.triggerHooksCreate(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter following details about the trigger webhook @@ -189,8 +190,8 @@ Class name: Trigger name: URL: https://Successfully created a "beforeSave" trigg h.Out.Reset() h.Err.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("bar\nafterSave\napi.example.com/bar/afterSave\n")) - ensure.Nil(t, tr.triggerHooksCreate(h.env, nil)) + h.Env.In = ioutil.NopCloser(strings.NewReader("bar\nafterSave\napi.example.com/bar/afterSave\n")) + ensure.Nil(t, tr.triggerHooksCreate(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter following details about the trigger webhook @@ -205,9 +206,9 @@ func TestTriggerHooksUpdate(t *testing.T) { h := newTriggersHarness(t) var tr triggerHooksCmd - h.env.In = + h.Env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\napi.example.com/_foo/beforeSave\n")) - ensure.Nil(t, tr.triggerHooksUpdate(h.env, nil)) + ensure.Nil(t, tr.triggerHooksUpdate(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter following details about the trigger webhook @@ -219,9 +220,9 @@ Class name: Trigger name: URL: https://Successfully update the "beforeSave" trig h.Out.Reset() h.Err.Reset() - h.env.In = + h.Env.In = ioutil.NopCloser(strings.NewReader("bar\nafterSave\napi.example.com/_bar/afterSave\n")) - ensure.Nil(t, tr.triggerHooksUpdate(h.env, nil)) + ensure.Nil(t, tr.triggerHooksUpdate(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter following details about the trigger webhook @@ -237,8 +238,8 @@ func TestTriggerHookDelete(t *testing.T) { h := newTriggersHarness(t) tr := triggerHooksCmd{interactive: true} - h.env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\ny\n")) - ensure.Nil(t, tr.triggerHooksDelete(h.env, nil)) + h.Env.In = ioutil.NopCloser(strings.NewReader("foo\nbeforeSave\ny\n")) + ensure.Nil(t, tr.triggerHooksDelete(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter following details about the trigger webhook @@ -247,8 +248,8 @@ Class name: Trigger name: Are you sure you want to delete "beforeSave" webhook t `) h.Out.Reset() - h.env.In = ioutil.NopCloser(strings.NewReader("foo\nn\n")) - ensure.Nil(t, tr.triggerHooksDelete(h.env, nil)) + h.Env.In = ioutil.NopCloser(strings.NewReader("foo\nn\n")) + ensure.Nil(t, tr.triggerHooksDelete(h.Env, nil)) ensure.DeepEqual(t, h.Out.String(), `Please enter following details about the trigger webhook diff --git a/update_cmd.go b/update.go similarity index 83% rename from update_cmd.go rename to update.go index 7a10ac8..6ab320e 100644 --- a/update_cmd.go +++ b/update.go @@ -6,6 +6,7 @@ import ( "net/url" "runtime" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" "github.com/inconshreveable/go-update" "github.com/kardianos/osext" @@ -22,7 +23,7 @@ const ( type updateCmd struct{} -func (u *updateCmd) latestVersion(e *env) (string, error) { +func (u *updateCmd) latestVersion(e *parsecli.Env) (string, error) { v := make(url.Values) v.Set("version", "latest") req := &http.Request{ @@ -41,7 +42,7 @@ func (u *updateCmd) latestVersion(e *env) (string, error) { return res.Version, nil } -func (u *updateCmd) getDownloadURL(e *env) (string, error) { +func (u *updateCmd) getDownloadURL(e *parsecli.Env) (string, error) { ostype := runtime.GOOS arch := runtime.GOARCH @@ -49,7 +50,7 @@ func (u *updateCmd) getDownloadURL(e *env) (string, error) { if err != nil { return "", err } - if latestVersion == "" || latestVersion == version { + if latestVersion == "" || latestVersion == parsecli.Version { return "", nil } @@ -69,7 +70,7 @@ func (u *updateCmd) getDownloadURL(e *env) (string, error) { return downloadURL, nil } -func (u *updateCmd) updateCLI(e *env) (bool, error) { +func (u *updateCmd) updateCLI(e *parsecli.Env) (bool, error) { downloadURL, err := u.getDownloadURL(e) if err != nil { return false, err @@ -96,23 +97,23 @@ func (u *updateCmd) updateCLI(e *env) (bool, error) { return true, nil } -func (u *updateCmd) run(e *env) error { +func (u *updateCmd) run(e *parsecli.Env) error { updated, err := u.updateCLI(e) if err != nil { return err } if !updated { - fmt.Fprintf(e.Out, "Already using the latest cli version: %s\n", version) + fmt.Fprintf(e.Out, "Already using the latest cli version: %s\n", parsecli.Version) } return nil } -func newUpdateCmd(e *env) *cobra.Command { +func NewUpdateCmd(e *parsecli.Env) *cobra.Command { var u updateCmd return &cobra.Command{ Use: "update", Short: "Updates this tool to the latest version", Long: "Updates this tool to the latest version.", - Run: runNoArgs(e, u.run), + Run: parsecli.RunNoArgs(e, u.run), } } diff --git a/update_cmd_test.go b/update_test.go similarity index 66% rename from update_cmd_test.go rename to update_test.go index e56cb26..0127a72 100644 --- a/update_cmd_test.go +++ b/update_test.go @@ -5,6 +5,7 @@ import ( "net/http" "testing" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/ensure" "github.com/facebookgo/jsonpipe" "github.com/facebookgo/parse" @@ -13,10 +14,10 @@ import ( func TestLatestVersion(t *testing.T) { t.Parallel() - h := newHarness(t) + h := parsecli.NewHarness(t) defer h.Stop() - ht := transportFunc(func(r *http.Request) (*http.Response, error) { + ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/supported") return &http.Response{ StatusCode: http.StatusOK, @@ -27,14 +28,14 @@ func TestLatestVersion(t *testing.T) { ), }, nil }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} + h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} u := new(updateCmd) - latestVersion, err := u.latestVersion(h.env) + latestVersion, err := u.latestVersion(h.Env) ensure.Nil(t, err) ensure.DeepEqual(t, latestVersion, "2.0.2") - downloadURL, err := u.getDownloadURL(h.env) + downloadURL, err := u.getDownloadURL(h.Env) ensure.StringContains(t, downloadURL, "https://github.com/ParsePlatform/parse-cli/releases/download/release_2.0.2") } diff --git a/utils.go b/utils.go index 753b6bc..21e8e04 100644 --- a/utils.go +++ b/utils.go @@ -3,81 +3,14 @@ package main import ( "errors" "fmt" - "net/http" "net/url" - "regexp" - "strconv" "strings" - "time" - "github.com/facebookgo/errgroup" - "github.com/facebookgo/parse" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/facebookgo/stackerr" ) -var numbers = regexp.MustCompile("[0-9]+") - -func numericLessThan(a, b string) bool { - aLen, bLen := len(a), len(b) - minLen := aLen - if bLen < minLen { - minLen = bLen - } - if minLen == 0 { - if aLen == 0 { - return bLen != 0 - } - return false - } - - i := 0 - for ; i < minLen && a[i] == b[i]; i++ { - } - if i == minLen { - if aLen == bLen { - return false - } - if aLen == minLen { - return true - } - return false - } - - a, b = a[i:], b[i:] - aNos, bNos := numbers.FindAllStringIndex(a, 1), numbers.FindAllStringIndex(b, 1) - - if len(aNos) == 1 && aNos[0][0] == 0 && - len(bNos) == 1 && bNos[0][0] == 0 { - aNum, err := strconv.Atoi(a[:aNos[0][1]]) - if err != nil { - return a < b - } - bNum, err := strconv.Atoi(b[:bNos[0][1]]) - if err != nil { - return a < b - } - if aNum != bNum { - return aNum < bNum - } - } - return a < b -} - -type naturalStrings []string - -func (v naturalStrings) Len() int { - return len(v) -} - -func (v naturalStrings) Less(i, j int) bool { - return !numericLessThan(v[i], v[j]) // desc order -} - -func (v naturalStrings) Swap(i, j int) { - v[i], v[j] = v[j], v[i] -} - -func getConfirmation(message string, e *env) bool { +func getConfirmation(message string, e *parsecli.Env) bool { fmt.Fprintf(e.Out, message) var confirm string fmt.Fscanf(e.In, "%s\n", &confirm) @@ -96,104 +29,3 @@ func validateURL(urlStr string) error { } return nil } - -func getHostFromURL(urlStr, email string) (string, error) { - netURL, err := url.Parse(urlStr) - if err != nil { - return "", stackerr.Wrap(err) - } - server := regexp.MustCompile(`(.*):\d+$`).ReplaceAllString(netURL.Host, "$1") - if server == "" { - return "", stackerr.Newf("%s is not a valid url", urlStr) - } - if email != "" { - return fmt.Sprintf("%s#%s", server, email), nil - } - return server, nil -} - -func checkIfSupported(e *env, version string, args ...string) (string, error) { - v := make(url.Values) - v.Set("version", version) - v.Set("other", strings.Join(args, " ")) - req := &http.Request{ - Method: "GET", - URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, - } - - type result struct { - warning string - err error - } - - timeout := make(chan *result, 1) - go func() { - var res struct { - Warning string `json:"warning"` - } - _, err := e.ParseAPIClient.Do(req, nil, &res) - timeout <- &result{warning: res.Warning, err: err} - }() - - select { - case res := <-timeout: - return res.warning, stackerr.Wrap(res.err) - case <-time.After(time.Duration(500) * time.Millisecond): - return "", nil - } - return "", nil -} - -func last4(str string) string { - l := len(str) - if l > 4 { - return fmt.Sprintf("%s%s", strings.Repeat("*", l-4), str[l-4:l]) - } - return str -} - -// errorString returns the error string with our without the stack trace -// depending on the environment variable. this exists because we want plain -// messages for end users, but when we're working on the CLI we want the stack -// trace for debugging. -func errorString(e *env, err error) string { - type hasUnderlying interface { - HasUnderlying() error - } - - parseErr := func(err error) error { - if apiErr, ok := err.(*parse.Error); ok { - return errors.New(apiErr.Message) - } - return err - } - - lastErr := func(err error) error { - if serr, ok := err.(*stackerr.Error); ok { - if errs := stackerr.Underlying(serr); len(errs) != 0 { - err = errs[len(errs)-1] - } - } else { - if eu, ok := err.(hasUnderlying); ok { - err = eu.HasUnderlying() - } - } - - return parseErr(err) - } - - if !e.ErrorStack { - if merr, ok := err.(errgroup.MultiError); ok { - var multiError []error - for _, ierr := range []error(merr) { - multiError = append(multiError, lastErr(ierr)) - } - err = errgroup.MultiError(multiError) - } else { - err = lastErr(err) - } - return parseErr(err).Error() - } - - return err.Error() -} diff --git a/utils_test.go b/utils_test.go deleted file mode 100644 index f3bb9b1..0000000 --- a/utils_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package main - -import ( - "crypto/rand" - "io/ioutil" - "net/http" - "regexp" - "testing" - - "github.com/facebookgo/ensure" - "github.com/facebookgo/jsonpipe" - "github.com/facebookgo/parse" -) - -func TestBothEmpty(t *testing.T) { - t.Parallel() - ensure.False(t, numericLessThan("", "")) -} - -func TestOnlyAEmpty(t *testing.T) { - t.Parallel() - ensure.True(t, numericLessThan("", "b")) -} - -func TestOnlyBEmpty(t *testing.T) { - t.Parallel() - ensure.False(t, numericLessThan("a", "")) -} - -func TestBothEqual(t *testing.T) { - t.Parallel() - ensure.False(t, numericLessThan("abc", "abc")) -} - -func TestAPrefixOfB(t *testing.T) { - t.Parallel() - ensure.True(t, numericLessThan("abc", "abcd")) -} - -func TestBPrefixOfA(t *testing.T) { - t.Parallel() - ensure.False(t, numericLessThan("abcd", "abc")) -} - -func TestABSansNumbers(t *testing.T) { - t.Parallel() - - const max = 10 - dictionary := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - var rb [max]byte - randomString := func(length int) string { - bytes := rb[:length] - _, err := rand.Read(bytes) - ensure.Nil(t, err) - for k, v := range bytes { - bytes[k] = dictionary[v%byte(len(dictionary))] - } - return string(bytes) - } - - for i := 1; i < max; i++ { - for j := 1; j < max; j++ { - a, b := randomString(i), randomString(j) - ensure.DeepEqual(t, numericLessThan(a, b), a < b) - } - } -} - -func TestABWithNumbers(t *testing.T) { - ensure.True(t, numericLessThan("abc9", "abc12")) - ensure.False(t, numericLessThan("abc12", "abc9")) - ensure.DeepEqual(t, numericLessThan("abc001", "abc1"), "abc001" < "abc1") -} - -func TestGetHostFromURL(t *testing.T) { - urlStr := "https://api.parse.com/1/" - host, err := getHostFromURL(urlStr, "") - ensure.Nil(t, err) - ensure.DeepEqual(t, host, "api.parse.com") - - urlStr = "https://api.parse.com/1/" - host, err = getHostFromURL(urlStr, "yolo@earth.com") - ensure.Nil(t, err) - ensure.DeepEqual(t, host, "api.parse.com#yolo@earth.com") - - urlStr = "https://api.example.com:8080/1/" - host, err = getHostFromURL(urlStr, "") - ensure.Nil(t, err) - ensure.DeepEqual(t, host, "api.example.com") - - urlStr = "https://api.example.com:8080/1/" - host, err = getHostFromURL(urlStr, "yolo@earth.com") - ensure.Nil(t, err) - ensure.DeepEqual(t, host, "api.example.com#yolo@earth.com") - - urlStr = "api.example.com:8080:90" - host, err = getHostFromURL(urlStr, "") - ensure.Err(t, err, regexp.MustCompile("not a valid url")) -} - -func TestIsSupportedWarning(t *testing.T) { - t.Parallel() - - h := newHarness(t) - defer h.Stop() - - ht := transportFunc(func(r *http.Request) (*http.Response, error) { - ensure.DeepEqual(t, r.URL.Path, "/1/supported") - return &http.Response{ - StatusCode: http.StatusOK, - Body: ioutil.NopCloser( - jsonpipe.Encode( - map[string]string{"warning": "please update"}, - ), - ), - }, nil - }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - message, err := checkIfSupported(h.env, "2.0.2") - ensure.Nil(t, err) - ensure.DeepEqual(t, message, "please update") -} - -func TestIsSupportedError(t *testing.T) { - t.Parallel() - - h := newHarness(t) - defer h.Stop() - - ht := transportFunc(func(r *http.Request) (*http.Response, error) { - ensure.DeepEqual(t, r.URL.Path, "/1/supported") - return &http.Response{ - StatusCode: http.StatusBadRequest, - Body: ioutil.NopCloser( - jsonpipe.Encode( - map[string]string{"error": "not supported"}, - ), - ), - }, nil - }) - h.env.ParseAPIClient = &ParseAPIClient{apiClient: &parse.Client{Transport: ht}} - _, err := checkIfSupported(h.env, "2.0.2") - ensure.Err(t, err, regexp.MustCompile("not supported")) -} - -func TestLastFour(t *testing.T) { - t.Parallel() - - ensure.DeepEqual(t, last4(""), "") - ensure.DeepEqual(t, last4("a"), "a") - ensure.DeepEqual(t, last4("ab"), "ab") - ensure.DeepEqual(t, last4("abc"), "abc") - ensure.DeepEqual(t, last4("abcd"), "abcd") - ensure.DeepEqual(t, last4("abcdefg"), "***defg") -} diff --git a/version_cmd.go b/version.go similarity index 58% rename from version_cmd.go rename to version.go index bbc335b..6569485 100644 --- a/version_cmd.go +++ b/version.go @@ -3,23 +3,24 @@ package main import ( "fmt" + "github.com/ParsePlatform/parse-cli/parsecli" "github.com/spf13/cobra" ) type versionCmd struct{} -func (c *versionCmd) run(e *env) error { - fmt.Fprintln(e.Out, version) +func (c *versionCmd) run(e *parsecli.Env) error { + fmt.Fprintln(e.Out, parsecli.Version) return nil } -func newVersionCmd(e *env) *cobra.Command { +func NewVersionCmd(e *parsecli.Env) *cobra.Command { var c versionCmd cmd := &cobra.Command{ Use: "version", Short: "Gets the Command Line Tools version", Long: `Gets the Command Line Tools version.`, - Run: runNoArgs(e, c.run), + Run: parsecli.RunNoArgs(e, c.run), Aliases: []string{"cliversion"}, } return cmd diff --git a/version_test.go b/version_test.go new file mode 100644 index 0000000..238936e --- /dev/null +++ b/version_test.go @@ -0,0 +1,19 @@ +package main + +import ( + "testing" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/ensure" +) + +func TestVersion(t *testing.T) { + t.Parallel() + h := parsecli.NewHarness(t) + defer h.Stop() + var c versionCmd + err := c.run(h.Env) + ensure.Nil(t, err) + ensure.DeepEqual(t, h.Out.String(), parsecli.Version+"\n") + ensure.DeepEqual(t, h.Err.String(), "") +} From 1a6cd11bfc79f630add2b233dd6ce9b67848758b Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 5 Oct 2015 00:01:02 +0000 Subject: [PATCH 067/106] reorganize new and add re-organize new & add, promote them to top-level, since they are generic for all platforms. --- parsecmd/add_common.go => add.go | 7 +-- parsecmd/add_test.go => add_test.go | 2 +- parsecmd/migrate.go => migrate.go | 2 +- parsecmd/migrate_test.go => migrate_test.go | 2 +- parsecmd/new_common.go => new.go | 42 +++--------------- parsecmd/new_test.go => new_test.go | 5 ++- parse_commands.go | 6 +-- parsecmd/add.go | 11 ++--- parsecmd/deploy.go | 2 +- parsecmd/deploy_test.go | 7 +++ parsecmd/jssdk.go | 2 +- parsecmd/new.go | 47 +++++++++++++++++++++ 12 files changed, 81 insertions(+), 54 deletions(-) rename parsecmd/add_common.go => add.go (91%) rename parsecmd/add_test.go => add_test.go (99%) rename parsecmd/migrate.go => migrate.go (99%) rename parsecmd/migrate_test.go => migrate_test.go (99%) rename parsecmd/new_common.go => new.go (89%) rename parsecmd/new_test.go => new_test.go (98%) create mode 100644 parsecmd/new.go diff --git a/parsecmd/add_common.go b/add.go similarity index 91% rename from parsecmd/add_common.go rename to add.go index 13d75ed..e5c61d6 100644 --- a/parsecmd/add_common.go +++ b/add.go @@ -1,7 +1,8 @@ -package parsecmd +package main import ( "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/ParsePlatform/parse-cli/parsecmd" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) @@ -24,7 +25,7 @@ func (a *addCmd) addSelectedApp( if !ok { return stackerr.New("invalid parse app config passed.") } - return a.addSelectedParseApp(name, parseAppConfig, args, e) + return parsecmd.AddSelectedParseApp(name, parseAppConfig, args, a.MakeDefault, a.verbose, e) } return stackerr.Newf("Unknown project type: %d.", e.Type) @@ -68,7 +69,7 @@ func (a *addCmd) run(e *parsecli.Env, args []string) error { if err != nil { return err } - appConfig := a.getParseAppConfig(app) + appConfig := parsecmd.GetParseAppConfig(app) return a.addSelectedApp(app.Name, appConfig, args, e) } diff --git a/parsecmd/add_test.go b/add_test.go similarity index 99% rename from parsecmd/add_test.go rename to add_test.go index bf9a499..c4c5ea2 100644 --- a/parsecmd/add_test.go +++ b/add_test.go @@ -1,4 +1,4 @@ -package parsecmd +package main import ( "encoding/json" diff --git a/parsecmd/migrate.go b/migrate.go similarity index 99% rename from parsecmd/migrate.go rename to migrate.go index 468a39b..b88f678 100644 --- a/parsecmd/migrate.go +++ b/migrate.go @@ -1,4 +1,4 @@ -package parsecmd +package main import ( "fmt" diff --git a/parsecmd/migrate_test.go b/migrate_test.go similarity index 99% rename from parsecmd/migrate_test.go rename to migrate_test.go index c71846d..10f7ead 100644 --- a/parsecmd/migrate_test.go +++ b/migrate_test.go @@ -1,4 +1,4 @@ -package parsecmd +package main import ( "io/ioutil" diff --git a/parsecmd/new_common.go b/new.go similarity index 89% rename from parsecmd/new_common.go rename to new.go index aabda08..c4f3e3d 100644 --- a/parsecmd/new_common.go +++ b/new.go @@ -1,4 +1,4 @@ -package parsecmd +package main import ( "fmt" @@ -7,6 +7,7 @@ import ( "strings" "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/ParsePlatform/parse-cli/parsecmd" "github.com/facebookgo/parse" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" @@ -209,40 +210,7 @@ Please type [y] if you wish to download the current Cloud Code or [n] for blank n.configOnly = true } } - - dumpTemplate := false - if !isNew && !n.configOnly { - // if parse app was already created try to fetch cloud code and populate dir - masterKey, err := appConfig.GetMasterKey(e) - if err != nil { - return false, err - } - e.ParseAPIClient = e.ParseAPIClient.WithCredentials( - parse.MasterKey{ - ApplicationID: appConfig.GetApplicationID(), - MasterKey: masterKey, - }, - ) - - d := &downloadCmd{destination: e.Root} - err = d.run(e, nil) - if err != nil { - if err == errNoFiles { - dumpTemplate = true - } else { - fmt.Fprintln( - e.Out, - ` -NOTE: If you like to fetch the latest deployed Cloud Code from Parse, -you can use the "parse download" command after finishing the set up. -This will download Cloud Code to a temporary location. -`, - ) - } - } - } - dumpTemplate = (isNew || dumpTemplate) && !n.configOnly - return dumpTemplate, parsecli.CloneSampleCloudCode(e, dumpTemplate) + return parsecmd.CloneSampleCloudCode(e, isNew, n.configOnly, appConfig) } return false, stackerr.Newf("Unknown project type: %d", e.Type) } @@ -270,7 +238,7 @@ func (n *newCmd) configureSample( ) if e.Type == parsecli.ParseFormat { - return useLatestJSSDK(e) + return parsecmd.UseLatestJSSDK(e) } return nil } @@ -323,7 +291,7 @@ func (n *newCmd) run(e *parsecli.Env) error { } e.Type = parsecli.ParseFormat - appConfig := addCmd.getParseAppConfig(app) + appConfig := parsecmd.GetParseAppConfig(app) dumpTemplate, err := n.setupSample(e, app.Name, appConfig, isNew, nonInteractive) if err != nil { diff --git a/parsecmd/new_test.go b/new_test.go similarity index 98% rename from parsecmd/new_test.go rename to new_test.go index 110d911..7b980b7 100644 --- a/parsecmd/new_test.go +++ b/new_test.go @@ -1,4 +1,4 @@ -package parsecmd +package main import ( "fmt" @@ -216,6 +216,9 @@ func TestSetupAndConfigure(t *testing.T) { ensure.Nil(t, err) ensure.True(t, code) + type jsSDKVersion struct { + JS []string `json:"js"` + } ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { ensure.DeepEqual(t, r.URL.Path, "/1/jsVersions") rows := jsSDKVersion{JS: []string{"1.5.0", "1.6.0"}} diff --git a/parse_commands.go b/parse_commands.go index 692a044..16c9194 100644 --- a/parse_commands.go +++ b/parse_commands.go @@ -26,7 +26,7 @@ http://parse.com`, }, } - c.AddCommand(parsecmd.NewAddCmd(e)) + c.AddCommand(NewAddCmd(e)) c.AddCommand(NewConfigureCmd(e)) c.AddCommand(NewDefaultCmd(e)) c.AddCommand(parsecmd.NewDeployCmd(e)) @@ -37,8 +37,8 @@ http://parse.com`, c.AddCommand(parsecmd.NewJsSdkCmd(e)) c.AddCommand(NewListCmd(e)) c.AddCommand(parsecmd.NewLogsCmd(e)) - c.AddCommand(parsecmd.NewMigrateCmd(e)) - c.AddCommand(parsecmd.NewNewCmd(e)) + c.AddCommand(NewMigrateCmd(e)) + c.AddCommand(NewNewCmd(e)) c.AddCommand(parsecmd.NewReleasesCmd(e)) c.AddCommand(parsecmd.NewRollbackCmd(e)) c.AddCommand(parsecmd.NewSymbolsCmd(e)) diff --git a/parsecmd/add.go b/parsecmd/add.go index 019b700..c9eb66a 100644 --- a/parsecmd/add.go +++ b/parsecmd/add.go @@ -7,17 +7,18 @@ import ( "github.com/facebookgo/stackerr" ) -func (a *addCmd) getParseAppConfig(app *parsecli.App) *parsecli.ParseAppConfig { +func GetParseAppConfig(app *parsecli.App) *parsecli.ParseAppConfig { pc := &parsecli.ParseAppConfig{ ApplicationID: app.ApplicationID, } return pc.WithInternalMasterKey(app.MasterKey) } -func (a *addCmd) addSelectedParseApp( +func AddSelectedParseApp( appName string, appConfig *parsecli.ParseAppConfig, args []string, + makeDefault, verbose bool, e *parsecli.Env, ) error { config, err := parsecli.ConfigFromDir(e.Root) @@ -48,7 +49,7 @@ func (a *addCmd) addSelectedParseApp( } } - if a.MakeDefault { + if makeDefault { if _, ok := parseConfig.Applications[parsecli.DefaultKey]; ok { return stackerr.New(`Default key already set. To override default, use command "parse default"`) } @@ -58,9 +59,9 @@ func (a *addCmd) addSelectedParseApp( if err := parsecli.StoreConfig(e, parseConfig); err != nil { return err } - if a.verbose { + if verbose { fmt.Fprintf(e.Out, "Written config for %q\n", appName) - if a.MakeDefault { + if makeDefault { fmt.Fprintf(e.Out, "Set %q as default\n", appName) } } diff --git a/parsecmd/deploy.go b/parsecmd/deploy.go index c443a70..d6c3e9f 100644 --- a/parsecmd/deploy.go +++ b/parsecmd/deploy.go @@ -363,7 +363,7 @@ func (d *deployCmd) deploy( fmt.Fprintln(e.Err, "JS SDK version not set, setting it to latest available JS SDK version", ) - if err := useLatestJSSDK(e); err != nil { + if err := UseLatestJSSDK(e); err != nil { return nil, err } } diff --git a/parsecmd/deploy_test.go b/parsecmd/deploy_test.go index 6628aae..047ca1a 100644 --- a/parsecmd/deploy_test.go +++ b/parsecmd/deploy_test.go @@ -1,6 +1,7 @@ package parsecmd import ( + "encoding/json" "fmt" "io/ioutil" "net/http" @@ -16,6 +17,12 @@ import ( "github.com/facebookgo/parse" ) +func jsonStr(t testing.TB, v interface{}) string { + b, err := json.Marshal(v) + ensure.Nil(t, err) + return string(b) +} + func createParseProject(t testing.TB) *parsecli.Harness { h := parsecli.NewHarness(t) h.MakeEmptyRoot() diff --git a/parsecmd/jssdk.go b/parsecmd/jssdk.go index 5dd74b3..2a534a3 100644 --- a/parsecmd/jssdk.go +++ b/parsecmd/jssdk.go @@ -90,7 +90,7 @@ func (j *jsSDKCmd) setVersion(e *parsecli.Env, c *parsecli.Context) error { // useLatestJSSDK is a utility method used by deploy & develop // to write set jssdk version to latest available, if none set -func useLatestJSSDK(e *parsecli.Env) error { +func UseLatestJSSDK(e *parsecli.Env) error { var j jsSDKCmd versions, err := j.getAllJSSdks(e) if err != nil { diff --git a/parsecmd/new.go b/parsecmd/new.go new file mode 100644 index 0000000..9028f56 --- /dev/null +++ b/parsecmd/new.go @@ -0,0 +1,47 @@ +package parsecmd + +import ( + "fmt" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/parse" +) + +func CloneSampleCloudCode( + e *parsecli.Env, + isNew, configOnly bool, + appConfig parsecli.AppConfig) (bool, error) { + dumpTemplate := false + if !isNew && !configOnly { + // if parse app was already created try to fetch cloud code and populate dir + masterKey, err := appConfig.GetMasterKey(e) + if err != nil { + return false, err + } + e.ParseAPIClient = e.ParseAPIClient.WithCredentials( + parse.MasterKey{ + ApplicationID: appConfig.GetApplicationID(), + MasterKey: masterKey, + }, + ) + + d := &downloadCmd{destination: e.Root} + err = d.run(e, nil) + if err != nil { + if err == errNoFiles { + dumpTemplate = true + } else { + fmt.Fprintln( + e.Out, + ` +NOTE: If you like to fetch the latest deployed Cloud Code from Parse, +you can use the "parse download" command after finishing the set up. +This will download Cloud Code to a temporary location. +`, + ) + } + } + } + dumpTemplate = (isNew || dumpTemplate) && !configOnly + return dumpTemplate, parsecli.CloneSampleCloudCode(e, dumpTemplate) +} From 37214d96605eeda439664575f29f280c57a30c85 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 5 Oct 2015 20:57:34 +0000 Subject: [PATCH 068/106] rename cloud code -> Cloud Code rename 'cloud code' to 'Cloud Code' since it's a proper noun --- default_test.go | 4 ++-- functions.go | 6 +++--- functions_test.go | 2 +- list_test.go | 8 ++++---- new.go | 2 +- parsecli/parse_config.go | 2 +- parsecmd/new.go | 2 +- parsecmd/releases.go | 2 +- parsecmd/releases_test.go | 4 ++-- triggers.go | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/default_test.go b/default_test.go index e82ad14..e1066ed 100644 --- a/default_test.go +++ b/default_test.go @@ -71,7 +71,7 @@ func TestSetDefaultInvalid(t *testing.T) { regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`), ) ensure.DeepEqual(t, h.Out.String(), - `The following apps are associated with cloud code in the current directory: + `The following apps are associated with Cloud Code in the current directory: * first second `) @@ -129,7 +129,7 @@ func TestLegacySetDefaultInvalid(t *testing.T) { regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`), ) ensure.DeepEqual(t, h.Out.String(), - `The following apps are associated with cloud code in the current directory: + `The following apps are associated with Cloud Code in the current directory: * first second `) diff --git a/functions.go b/functions.go index 5e9eb70..4798c62 100644 --- a/functions.go +++ b/functions.go @@ -187,7 +187,7 @@ func (h *functionHooksCmd) functionHooksDelete(e *parsecli.Env, ctx *parsecli.Co } fmt.Fprintf(e.Out, "Successfully deleted webhook function %q\n", params.FunctionName) if res.FunctionName != "" { - fmt.Fprintf(e.Out, "Function %q defined in cloud code will be used henceforth\n", res.FunctionName) + fmt.Fprintf(e.Out, "Function %q defined in Cloud Code will be used henceforth\n", res.FunctionName) } } @@ -205,8 +205,8 @@ func NewFunctionHooksCmd(e *parsecli.Env) *cobra.Command { c := &cobra.Command{ Use: "functions", - Short: "List cloud code functions and function webhooks", - Long: "List cloud code functions and function webhooks", + Short: "List Cloud Code functions and function webhooks", + Long: "List Cloud Code functions and function webhooks", Run: parsecli.RunWithClient(e, h.functionHooks), } diff --git a/functions_test.go b/functions_test.go index 343ca79..c7f527c 100644 --- a/functions_test.go +++ b/functions_test.go @@ -226,7 +226,7 @@ func TestFunctionHookDelete(t *testing.T) { ensure.DeepEqual(t, h.Out.String(), `Please enter the function name: Are you sure you want to delete webhook function: "foo" (y/n): Successfully deleted webhook function "foo" -Function "foo" defined in cloud code will be used henceforth +Function "foo" defined in Cloud Code will be used henceforth `) h.Out.Reset() diff --git a/list_test.go b/list_test.go index 5184606..0b302e2 100644 --- a/list_test.go +++ b/list_test.go @@ -21,7 +21,7 @@ func TestPrintListOneAppNoDefaultKey(t *testing.T) { h := parsecli.NewHarness(t) defer h.Stop() conf.PrettyPrintApps(h.Env) - ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with cloud code in the current directory:\n first\n") + ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with Cloud Code in the current directory:\n first\n") } func TestPrintListOneAppWithDefaultKey(t *testing.T) { @@ -32,7 +32,7 @@ func TestPrintListOneAppWithDefaultKey(t *testing.T) { h := parsecli.NewHarness(t) defer h.Stop() conf.PrettyPrintApps(h.Env) - ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with cloud code in the current directory:\n* first\n") + ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with Cloud Code in the current directory:\n* first\n") } func TestPrintListTwoAppsWithDefaultKey(t *testing.T) { @@ -43,7 +43,7 @@ func TestPrintListTwoAppsWithDefaultKey(t *testing.T) { h := parsecli.NewHarness(t) defer h.Stop() conf.PrettyPrintApps(h.Env) - ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with cloud code in the current directory:\n* first\n second\n") + ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with Cloud Code in the current directory:\n* first\n second\n") } func TestPrintListTwoAppsWithLinks(t *testing.T) { @@ -53,7 +53,7 @@ func TestPrintListTwoAppsWithLinks(t *testing.T) { h := parsecli.NewHarness(t) defer h.Stop() conf.PrettyPrintApps(h.Env) - ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with cloud code in the current directory:\n* first\n second -> first\n") + ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with Cloud Code in the current directory:\n* first\n second -> first\n") } func TestPrintListNoConfig(t *testing.T) { t.Parallel() diff --git a/new.go b/new.go index c4f3e3d..534341e 100644 --- a/new.go +++ b/new.go @@ -20,7 +20,7 @@ type newCmd struct { configOnly bool // create a Cloud Code project with only configuration, no code createNewApp bool // create a new app parseAppName string // name of parse app - codeLocation string // location of cloud code project + codeLocation string // location of Cloud Code project } func (n *newCmd) curlCommand(app *parsecli.App) string { diff --git a/parsecli/parse_config.go b/parsecli/parse_config.go index e37ce9e..27e272d 100644 --- a/parsecli/parse_config.go +++ b/parsecli/parse_config.go @@ -121,7 +121,7 @@ func (c *ParseConfig) PrettyPrintApps(e *Env) { fmt.Fprintln( e.Out, - "The following apps are associated with cloud code in the current directory:", + "The following apps are associated with Cloud Code in the current directory:", ) for _, appName := range appNames { diff --git a/parsecmd/new.go b/parsecmd/new.go index 9028f56..a0e09f6 100644 --- a/parsecmd/new.go +++ b/parsecmd/new.go @@ -13,7 +13,7 @@ func CloneSampleCloudCode( appConfig parsecli.AppConfig) (bool, error) { dumpTemplate := false if !isNew && !configOnly { - // if parse app was already created try to fetch cloud code and populate dir + // if parse app was already created try to fetch Cloud Code and populate dir masterKey, err := appConfig.GetMasterKey(e) if err != nil { return false, err diff --git a/parsecmd/releases.go b/parsecmd/releases.go index 393f349..68615f7 100644 --- a/parsecmd/releases.go +++ b/parsecmd/releases.go @@ -62,7 +62,7 @@ Note that you can list files for all releases shown in "parse releases"`, return stackerr.Wrap(err) } if len(versionFileNames.Cloud) != 0 { - fmt.Fprintf(e.Out, "Deployed cloud code files:\n") + fmt.Fprintf(e.Out, "Deployed Cloud Code files:\n") r.printFileNames(versionFileNames.Cloud, e) } if len(versionFileNames.Cloud) != 0 && len(versionFileNames.Public) != 0 { diff --git a/parsecmd/releases_test.go b/parsecmd/releases_test.go index 346d2a8..3e85a3b 100644 --- a/parsecmd/releases_test.go +++ b/parsecmd/releases_test.go @@ -83,7 +83,7 @@ func TestReleasesCmdPrintVersion(t *testing.T) { h.Out.Reset() err = r.printFiles("v1", releases, h.Env) ensure.Nil(t, err) - ensure.DeepEqual(t, h.Out.String(), `Deployed cloud code files: + ensure.DeepEqual(t, h.Out.String(), `Deployed Cloud Code files: app.js main.js views/index.js @@ -92,7 +92,7 @@ views/index.js h.Out.Reset() err = r.printFiles("v2", releases, h.Env) ensure.Nil(t, err) - ensure.DeepEqual(t, h.Out.String(), `Deployed cloud code files: + ensure.DeepEqual(t, h.Out.String(), `Deployed Cloud Code files: app.js main.js views/docs.js diff --git a/triggers.go b/triggers.go index d3b3fcb..db3d572 100644 --- a/triggers.go +++ b/triggers.go @@ -214,8 +214,8 @@ func NewTriggerHooksCmd(e *parsecli.Env) *cobra.Command { c := &cobra.Command{ Use: "triggers", - Short: "List cloud code triggers and trigger webhooks", - Long: "List cloud code triggers and trigger webhooks", + Short: "List Cloud Code triggers and trigger webhooks", + Long: "List Cloud Code triggers and trigger webhooks", Run: parsecli.RunWithClient(e, h.triggerHooks), } From f2d808cb5eeea12293dcdb59caac2589b5a3a10b Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 15 Oct 2015 14:53:59 +0000 Subject: [PATCH 069/106] [webhooks] move webhooks code into its own package - move webhooks related functions and commands into their own package --- configure.go | 425 +--------------- configure_test.go | 425 ---------------- parse_commands.go | 5 +- functions.go => webhooks/functions.go | 10 +- .../functions_test.go | 2 +- webhooks/hooks.go | 458 ++++++++++++++++++ webhooks/hooks_test.go | 436 +++++++++++++++++ triggers.go => webhooks/triggers.go | 2 +- triggers_test.go => webhooks/triggers_test.go | 2 +- 9 files changed, 914 insertions(+), 851 deletions(-) rename functions.go => webhooks/functions.go (95%) rename functions_test.go => webhooks/functions_test.go (99%) create mode 100644 webhooks/hooks.go create mode 100644 webhooks/hooks_test.go rename triggers.go => webhooks/triggers.go (99%) rename triggers_test.go => webhooks/triggers_test.go (99%) diff --git a/configure.go b/configure.go index 4b474f5..8fb3d6d 100644 --- a/configure.go +++ b/configure.go @@ -1,57 +1,24 @@ package main import ( - "bufio" - "errors" "fmt" "io" - "io/ioutil" - "net/url" "os" - "path" "sort" "strconv" - "strings" "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/ParsePlatform/parse-cli/webhooks" "github.com/facebookgo/stackerr" "github.com/spf13/cobra" ) -var ( - errInvalidFormat = errors.New( - ` -invalid format. -valid formats should look like: -[put|post],functionName,https_url -delete,functionName - -[put|post],className:triggerName,https_url -delete,className:triggerName -`) - - errPostToPut = errors.New( - `a hook with given name already exists: cannot create a new one. -`) - - errPutToPost = errors.New( - `a hook with the given name does not exist yet: cannot update the url. -`) - - errNotExist = errors.New( - `a hook with the given name does not exist. cannot delete it. - `) -) - type configureCmd struct { login parsecli.Login isDefault bool tokenReader io.Reader // for testing - // for hooks sub-command - hooksStrict bool - baseURL string - baseWebhookURL *url.URL + hooks webhooks.Hooks } func (c *configureCmd) accountKey(e *parsecli.Env) error { @@ -211,388 +178,6 @@ Enter a number between 1 and %d: `, return nil } -type hookOperation struct { - method string - function *functionHook - trigger *triggerHook -} - -func (c *configureCmd) checkTriggerName(s string) error { - switch strings.ToLower(s) { - case "beforesave", "beforedelete", "aftersave", "afterdelete": - return nil - } - return stackerr.Newf( - `invalid trigger name: %v. - This is the list of valid trigger names: - beforeSave - afterSave - beforeDelete - afterDelete -`, - s, - ) -} - -func (c *configureCmd) postOrPutHook( - e *parsecli.Env, - hooksOps []*hookOperation, - fields ...string, -) (bool, []*hookOperation, error) { - restOp := strings.ToUpper(fields[0]) - if restOp != "POST" && restOp != "PUT" { - return false, nil, stackerr.Wrap(errInvalidFormat) - } - - switch len(fields) { - case 3: - hooksOps = append(hooksOps, &hookOperation{ - method: restOp, - function: &functionHook{FunctionName: fields[1], URL: fields[2]}, - }) - return true, hooksOps, nil - - case 4: - if err := c.checkTriggerName(fields[2]); err != nil { - return false, nil, err - } - hooksOps = append(hooksOps, &hookOperation{ - method: restOp, - trigger: &triggerHook{ClassName: fields[1], TriggerName: fields[2], URL: fields[3]}, - }) - return true, hooksOps, nil - } - return false, nil, stackerr.Wrap(errInvalidFormat) - -} - -func (c *configureCmd) deleteHook( - e *parsecli.Env, - hooksOps []*hookOperation, - fields ...string, -) (bool, []*hookOperation, error) { - restOp := strings.ToUpper(fields[0]) - if restOp != "DELETE" { - return false, nil, stackerr.Wrap(errInvalidFormat) - } - - switch len(fields) { - case 2: - hooksOps = append(hooksOps, &hookOperation{ - method: "DELETE", - function: &functionHook{FunctionName: fields[1]}, - }) - return true, hooksOps, nil - case 3: - if err := c.checkTriggerName(fields[2]); err != nil { - return false, nil, err - } - hooksOps = append(hooksOps, &hookOperation{ - method: "DELETE", - trigger: &triggerHook{ClassName: fields[1], TriggerName: fields[2]}, - }) - return true, hooksOps, nil - } - - return false, nil, stackerr.Wrap(errInvalidFormat) -} - -func (c *configureCmd) appendHookOperation( - e *parsecli.Env, - fields []string, - hooks []*hookOperation, -) (bool, []*hookOperation, error) { - if len(fields) == 0 { - return false, hooks, nil - } - - switch strings.ToLower(fields[0]) { - case "post", "put": - return c.postOrPutHook(e, hooks, fields...) - - case "delete": - return c.deleteHook(e, hooks, fields...) - } - return false, nil, stackerr.Wrap(errInvalidFormat) -} - -func (c *configureCmd) processHooksOperation(e *parsecli.Env, op string) ([]string, error) { - op = strings.TrimSpace(op) - if op == "" { - return nil, nil - } - - fields := strings.SplitN(op, ",", 3) - switch restOp := strings.ToLower(fields[0]); restOp { - case "post", "put": - if len(fields) < 3 { - return nil, stackerr.Wrap(errInvalidFormat) - } - case "delete": - if len(fields) != 2 { - return nil, stackerr.Wrap(errInvalidFormat) - } - subFields := strings.SplitN(fields[1], ":", 2) - if len(subFields) == 2 { - fields = append(fields, subFields[1]) - fields[1] = subFields[0] - } - return fields, nil - default: - return nil, stackerr.Wrap(errInvalidFormat) - } - - if c.baseWebhookURL != nil { - u, err := c.baseWebhookURL.Parse(fields[2]) - if err != nil { - return nil, stackerr.Wrap(err) - } - fields[2] = u.String() - } - - switch subFields := strings.SplitN(fields[1], ":", 2); len(subFields) { - case 1: - return fields, nil - case 2: - fields = append(fields, fields[2]) - fields[3] = fields[2] - fields[2] = subFields[1] - fields[1] = subFields[0] - return fields, nil - } - return nil, stackerr.Wrap(errInvalidFormat) -} - -func (c *configureCmd) createHooksOperations( - e *parsecli.Env, - reader io.Reader, -) ([]*hookOperation, error) { - scanner := bufio.NewScanner(reader) - var ( - hooksOps []*hookOperation - added bool - ) - lineNum := 0 - for scanner.Scan() { - lineNum++ - fields, err := c.processHooksOperation(e, scanner.Text()) - if err != nil { - return nil, err - } - added, hooksOps, err = c.appendHookOperation(e, fields, hooksOps) - if err != nil { - return nil, err - } - if !added { - fmt.Fprintf(e.Out, "Ignoring line: %d\n", lineNum) - } - } - if err := scanner.Err(); err != nil { - return nil, stackerr.Wrap(err) - } - return hooksOps, nil -} - -func (c *configureCmd) checkStrictMode(restOp string, exists bool) (string, bool, error) { - restOp = strings.ToUpper(restOp) - if !exists { - if restOp == "PUT" { - if c.hooksStrict { - return "", false, stackerr.Wrap(errPutToPost) - } - return "POST", true, nil - } - if restOp == "DELETE" { - if c.hooksStrict { - return "", false, stackerr.Wrap(errNotExist) - } - return "DELETE", true, nil - } - } else if restOp == "POST" { - if c.hooksStrict { - return "", false, stackerr.Wrap(errPostToPut) - } - return "PUT", true, nil - } - return restOp, false, nil -} - -func (c *configureCmd) functionHookExists(e *parsecli.Env, name string) (bool, error) { - functionsURL, err := url.Parse(path.Join(defaultFunctionsURL, name)) - if err != nil { - return false, stackerr.Wrap(err) - } - var results struct { - Results []*functionHook `json:"results,omitempty"` - } - _, err = e.ParseAPIClient.Get(functionsURL, &results) - if err != nil { - if strings.Contains(err.Error(), "is defined") { - return false, nil - } - return false, stackerr.Wrap(err) - } - for _, result := range results.Results { - if result.URL != "" && result.FunctionName == name { - return true, nil - } - } - return false, nil -} - -func (c *configureCmd) deployFunctionHook(e *parsecli.Env, op *hookOperation) error { - if op.function == nil { - return stackerr.New("cannot deploy nil function hook") - } - exists, err := c.functionHookExists(e, op.function.FunctionName) - if err != nil { - return err - } - - restOp, suppressed, err := c.checkStrictMode(op.method, exists) - if err != nil { - return err - } - - function := &functionHooksCmd{Function: op.function} - switch restOp { - case "POST": - return function.functionHooksCreate(e, nil) - case "PUT": - return function.functionHooksUpdate(e, nil) - case "DELETE": - if suppressed { - return nil - } - return function.functionHooksDelete(e, nil) - } - return stackerr.Wrap(errInvalidFormat) -} - -func (c *configureCmd) triggerHookExists(e *parsecli.Env, className, triggerName string) (bool, error) { - triggersURL, err := url.Parse(path.Join(defaultTriggersURL, className, triggerName)) - if err != nil { - return false, stackerr.Wrap(err) - } - var results struct { - Results []*triggerHook `json:"results,omitempty"` - } - _, err = e.ParseAPIClient.Get(triggersURL, &results) - if err != nil { - if strings.Contains(err.Error(), "is defined") { - return false, nil - } - return false, stackerr.Wrap(err) - } - for _, result := range results.Results { - if result.URL != "" && result.ClassName == className && result.TriggerName == triggerName { - return true, nil - } - } - return false, nil -} - -func (c *configureCmd) deployTriggerHook(e *parsecli.Env, op *hookOperation) error { - if op.trigger == nil { - return stackerr.New("cannot deploy nil trigger hook") - } - - exists, err := c.triggerHookExists(e, op.trigger.ClassName, op.trigger.TriggerName) - if err != nil { - return err - } - restOp, suppressed, err := c.checkStrictMode(op.method, exists) - if err != nil { - return err - } - - trigger := &triggerHooksCmd{Trigger: op.trigger, All: false} - switch restOp { - case "POST": - return trigger.triggerHooksCreate(e, nil) - case "PUT": - return trigger.triggerHooksUpdate(e, nil) - case "DELETE": - if suppressed { - return nil - } - return trigger.triggerHooksDelete(e, nil) - } - return stackerr.Wrap(errInvalidFormat) - -} - -func (c *configureCmd) deployWebhooksConfig(e *parsecli.Env, hooksOps []*hookOperation) error { - for _, op := range hooksOps { - if op.function == nil && op.trigger == nil { - return stackerr.New("hook operation is neither a function, not a trigger.") - } - if op.function != nil && op.trigger != nil { - return stackerr.New("a hook cannot be both a function and a trigger.") - } - if op.function != nil { - if err := c.deployFunctionHook(e, op); err != nil { - return err - } - } else { - if err := c.deployTriggerHook(e, op); err != nil { - return err - } - } - fmt.Fprintln(e.Out) - } - return nil -} - -func (c *configureCmd) parseBaseURL(e *parsecli.Env) error { - if c.baseURL != "" { - u, err := url.Parse(c.baseURL) - if err != nil { - fmt.Fprintln(e.Err, "Invalid base webhook url provided") - return stackerr.Wrap(err) - } - if u.Scheme != "https" { - return stackerr.New("Please provide a valid https url") - } - c.baseWebhookURL = u - } - return nil -} - -func (c *configureCmd) hooksCmd(e *parsecli.Env, ctx *parsecli.Context, args []string) error { - if len(args) > 1 { - return fmt.Errorf("Invalid args: %v, only an optional hooks config file is expected.", args) - } - if err := c.parseBaseURL(e); err != nil { - return err - } - - reader := e.In - if len(args) == 1 { - file, err := os.Open(args[0]) - if err != nil { - return stackerr.Wrap(err) - } - reader = ioutil.NopCloser(file) - } else { - fmt.Fprintln(e.Out, "Since a webhooks config file was not provided reading from stdin.") - } - hooksOps, err := c.createHooksOperations(e, reader) - if err != nil { - return err - } - err = c.deployWebhooksConfig(e, hooksOps) - if err != nil { - fmt.Fprintln( - e.Out, - "Failed to deploy the webhooks config. Please try again...", - ) - return err - } - fmt.Fprintln(e.Out, "Successfully configured the given webhooks for the app.") - return nil -} - func NewConfigureCmd(e *parsecli.Env) *cobra.Command { var c configureCmd @@ -639,12 +224,12 @@ func NewConfigureCmd(e *parsecli.Env) *cobra.Command { Long: `Configure webhooks for the app based on the given configuration csv file. For more details read: https://parse.com/docs/js/guide#command-line-webhooks `, - Run: parsecli.RunWithArgsClient(e, c.hooksCmd), + Run: parsecli.RunWithArgsClient(e, c.hooks.HooksCmd), Aliases: []string{"webhooks"}, } - hooksCmd.Flags().BoolVarP(&c.hooksStrict, "strict", "s", c.hooksStrict, + hooksCmd.Flags().BoolVarP(&c.hooks.HooksStrict, "strict", "s", c.hooks.HooksStrict, "Configure hooks in strict mode, i.e., do not automatically fix errors.") - hooksCmd.Flags().StringVarP(&c.baseURL, "base", "b", c.baseURL, + hooksCmd.Flags().StringVarP(&c.hooks.BaseURL, "base", "b", c.hooks.BaseURL, `Base url to use while parsing the webhook url field. If provided, the config file can have relative urls.`) cmd.AddCommand(hooksCmd) diff --git a/configure_test.go b/configure_test.go index f05c363..a60f546 100644 --- a/configure_test.go +++ b/configure_test.go @@ -2,8 +2,6 @@ package main import ( "io/ioutil" - "net/url" - "os" "path/filepath" "regexp" "strings" @@ -152,426 +150,3 @@ func TestProjectType(t *testing.T) { ensure.StringContains(t, h.Out.String(), "Successfully set project type to: parse") ensure.Nil(t, err) } - -func TestCheckTriggerName(t *testing.T) { - t.Parallel() - - c := &configureCmd{} - - ensure.Nil(t, c.checkTriggerName("beforeSave")) - ensure.Nil(t, c.checkTriggerName("afterSave")) - ensure.Nil(t, c.checkTriggerName("beforeDelete")) - ensure.Nil(t, c.checkTriggerName("afterDelete")) - - ensure.Nil(t, c.checkTriggerName("BeforeSAVE")) - ensure.Nil(t, c.checkTriggerName("AFTERsave")) - ensure.Nil(t, c.checkTriggerName("BeforeDELETE")) - ensure.Nil(t, c.checkTriggerName("AFTERdelete")) - - ensure.Err(t, c.checkTriggerName("invalid"), regexp.MustCompile("list of valid trigger names")) -} - -func TestPostOrPutHook(t *testing.T) { - t.Parallel() - h := parsecli.NewHarness(t) - defer h.Stop() - - c := &configureCmd{} - - _, _, err := c.postOrPutHook(h.Env, nil, "other") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, _, err = c.postOrPutHook(h.Env, nil, "post", "1") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, _, err = c.postOrPutHook(h.Env, nil, "put", "1", "2", "3", "4") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - var hooksOps []*hookOperation - _, ops, err := c.postOrPutHook(h.Env, hooksOps, - "post", "call", "https://api.twilio.com/call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{ - FunctionName: "call", - URL: "https://api.twilio.com/call", - }, - ) - - _, ops, err = c.postOrPutHook(h.Env, hooksOps, - "put", "call", "https://api.twilio.com/call_1") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{ - FunctionName: "call", - URL: "https://api.twilio.com/call_1", - }, - ) - - _, ops, err = c.postOrPutHook(h.Env, hooksOps, "post", - "_User", "beforeSave", "https://api.twilio.com/message") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ - ClassName: "_User", - TriggerName: "beforeSave", - URL: "https://api.twilio.com/message", - }, - ) - - _, ops, err = c.postOrPutHook(h.Env, hooksOps, "put", - "_User", "beforeSave", "https://api.twilio.com/message_1") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ - ClassName: "_User", - TriggerName: "beforeSave", - URL: "https://api.twilio.com/message_1", - }, - ) - - _, ops, err = c.postOrPutHook(h.Env, hooksOps, "post", - "_User", "invalid", "https://other") - ensure.Err(t, err, regexp.MustCompile("invalid trigger")) -} - -func TestDeleteHook(t *testing.T) { - t.Parallel() - h := parsecli.NewHarness(t) - defer h.Stop() - - c := &configureCmd{} - _, _, err := c.deleteHook(h.Env, nil, "delete") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, _, err = c.deleteHook(h.Env, nil, "invalid", "1") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, _, err = c.deleteHook(h.Env, nil, "delete", "1", "2") - ensure.Err(t, err, regexp.MustCompile("invalid trigger name")) - - var hooksOps []*hookOperation - _, ops, err := c.deleteHook(h.Env, hooksOps, "delete", "call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{FunctionName: "call"}, - ) - - _, ops, err = c.deleteHook(h.Env, hooksOps, "delete", "_User", "beforeSave") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ClassName: "_User", TriggerName: "beforeSave"}, - ) -} - -func TestProcessHookOperation(t *testing.T) { - t.Parallel() - - h := parsecli.NewHarness(t) - defer h.Stop() - - c := &configureCmd{} - - ops, err := c.processHooksOperation(h.Env, "\n\t ") - ensure.Nil(t, err) - ensure.DeepEqual(t, len(ops), 0) - - _, err = c.processHooksOperation(h.Env, "invalid") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, err = c.processHooksOperation(h.Env, "delete,call,caller") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - ops, err = c.processHooksOperation(h.Env, "delete,call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"delete", "call"}) - - ops, err = c.processHooksOperation(h.Env, "delete,_User:beforeSave") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"delete", "_User", "beforeSave"}) - - ops, err = c.processHooksOperation(h.Env, "post,call,https://twilio.com/call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) - - ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2"}) - - ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2,call_3") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) - - ops, err = c.processHooksOperation(h.Env, - "pUt,_User:afterDelete,https://twilio.com/message_1,message_2") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"pUt", "_User", "afterDelete", - "https://twilio.com/message_1,message_2"}) - - u, err := url.Parse("https://parse.com") - ensure.Nil(t, err) - c.baseWebhookURL = u - - ops, err = c.processHooksOperation(h.Env, "post,call,https://parse.com/call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"post", "call", "https://parse.com/call"}) - - ops, err = c.processHooksOperation(h.Env, "post,call,https://twilio.com/call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) - - ops, err = c.processHooksOperation(h.Env, "put,call,/call_1,call_2,call_3") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"put", "call", "https://parse.com/call_1,call_2,call_3"}) - - ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2,call_3") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) -} - -func TestAppendHookOperation(t *testing.T) { - t.Parallel() - - h := parsecli.NewHarness(t) - defer h.Stop() - - c := &configureCmd{} - - var hooksOps []*hookOperation - _, ops, err := c.appendHookOperation(h.Env, nil, hooksOps) - ensure.Nil(t, err) - ensure.DeepEqual(t, hooksOps, ops) - - _, _, err = c.appendHookOperation(h.Env, []string{"invalid"}, nil) - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, ops, err = c.appendHookOperation(h.Env, - []string{"post", "call", "https://twilio.com/call"}, hooksOps) - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{FunctionName: "call", URL: "https://twilio.com/call"}, - ) - - _, ops, err = c.appendHookOperation(h.Env, - []string{"put", "call", "https://twilio.com/call_1"}, hooksOps) - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{FunctionName: "call", URL: "https://twilio.com/call_1"}, - ) - - //random stuff - _, ops, err = c.appendHookOperation(h.Env, - []string{"posT", "_User", "afterDelete", "https://twilio.com/message"}, hooksOps) - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message"}, - ) - - _, ops, err = c.appendHookOperation(h.Env, - []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1"}, hooksOps) - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1"}, - ) - - // random stuff - _, ops, err = c.appendHookOperation(h.Env, - []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1,message_2"}, hooksOps) - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1,message_2"}, - ) -} - -func TestCheckStrictMode(t *testing.T) { - t.Parallel() - - testCases := []struct { - restOp string - exists bool - strictMode bool - - op string - updated bool - err *regexp.Regexp - }{ - {exists: false, restOp: "put", strictMode: false, op: "POST", updated: true, err: nil}, - {exists: false, restOp: "post", strictMode: false, op: "POST", updated: false, err: nil}, - {exists: false, restOp: "delete", strictMode: false, op: "DELETE", updated: true, err: nil}, - {exists: true, restOp: "put", strictMode: false, op: "PUT", updated: false, err: nil}, - {exists: true, restOp: "post", strictMode: false, op: "PUT", updated: true, err: nil}, - {exists: true, restOp: "delete", strictMode: false, op: "DELETE", updated: false, err: nil}, - - {exists: false, restOp: "put", strictMode: true, err: regexp.MustCompile("does not exist")}, - {exists: false, restOp: "post", strictMode: true, op: "POST", updated: false, err: nil}, - {exists: false, restOp: "delete", strictMode: true, err: regexp.MustCompile("cannot delete")}, - {exists: true, restOp: "put", strictMode: true, op: "PUT", updated: false, err: nil}, - {exists: true, restOp: "post", strictMode: true, err: regexp.MustCompile("already exists")}, - {exists: true, restOp: "delete", strictMode: true, op: "DELETE", updated: false, err: nil}, - } - - c := &configureCmd{} - for _, testCase := range testCases { - c.hooksStrict = testCase.strictMode - op, updated, err := c.checkStrictMode(testCase.restOp, testCase.exists) - if testCase.err == nil { - ensure.Nil(t, err) - ensure.DeepEqual(t, op, testCase.op) - ensure.DeepEqual(t, updated, testCase.updated) - } else { - ensure.Err(t, err, testCase.err) - } - } -} - -func TestCreateHookOperations(t *testing.T) { - t.Parallel() - h := parsecli.NewHarness(t) - h.MakeEmptyRoot() - defer h.Stop() - - hooksConfigFile := filepath.Join(h.Env.Root, "webhooks.csv") - err := ioutil.WriteFile( - hooksConfigFile, - []byte{}, - 0666, - ) - ensure.Nil(t, err) - - c := &configureCmd{} - - f, err := os.Open(hooksConfigFile) - ensure.Nil(t, err) - ops, err := c.createHooksOperations(h.Env, f) - ensure.Nil(t, err) - ensure.DeepEqual(t, len(ops), 0) - ensure.Nil(t, f.Close()) - - err = ioutil.WriteFile( - hooksConfigFile, - []byte("\n"), - 0666, - ) - - f, err = os.Open(hooksConfigFile) - ensure.Nil(t, err) - ops, err = c.createHooksOperations(h.Env, f) - ensure.Nil(t, err) - ensure.DeepEqual(t, len(ops), 0) - ensure.DeepEqual(t, h.Out.String(), "Ignoring line: 1\n") - ensure.Nil(t, f.Close()) - h.Out.Reset() -} - -func TestFunctionHookExists(t *testing.T) { - t.Parallel() - h := newFunctionsHarness(t) - c := &configureCmd{} - exists, err := c.functionHookExists(h.Env, "foo") - ensure.Nil(t, err) - ensure.True(t, exists) - - exists, err = c.functionHookExists(h.Env, "bar") - ensure.Nil(t, err) - ensure.False(t, exists) -} - -func TestDeployFunctionHooks(t *testing.T) { - t.Parallel() - h := newFunctionsHarness(t) - c := &configureCmd{} - - err := c.deployFunctionHook( - h.Env, - &hookOperation{ - method: "post", - function: &functionHook{ - FunctionName: "foo", - URL: "https://api.example.com/foo", - }, - }, - ) - // not an error -> will be converted to put - ensure.Nil(t, err) -} - -func TestTriggerHookExists(t *testing.T) { - t.Parallel() - h := newTriggersHarness(t) - c := &configureCmd{} - exists, err := c.triggerHookExists(h.Env, "foo", "beforeSave") - ensure.Nil(t, err) - ensure.True(t, exists) - - exists, err = c.triggerHookExists(h.Env, "bar", "other") - ensure.Nil(t, err) - ensure.False(t, exists) -} - -func TestDeployTriggerHooks(t *testing.T) { - t.Parallel() - h := newTriggersHarness(t) - c := &configureCmd{} - - err := c.deployTriggerHook( - h.Env, - &hookOperation{ - method: "post", - trigger: &triggerHook{ - ClassName: "foo", - TriggerName: "beforeSave", - URL: "https://api.example.com/foo", - }, - }, - ) - // not an error -> will be converted to put - ensure.Nil(t, err) -} - -func TestParseBaseURL(t *testing.T) { - t.Parallel() - - h := parsecli.NewHarness(t) - defer h.Stop() - - c := &configureCmd{} - c.baseURL = "http://hello" - ensure.Err(t, c.parseBaseURL(h.Env), regexp.MustCompile("valid https url")) - - c.baseURL = "https://hello" - ensure.Nil(t, c.parseBaseURL(h.Env)) - ensure.DeepEqual(t, c.baseWebhookURL.String(), c.baseURL) -} diff --git a/parse_commands.go b/parse_commands.go index 16c9194..fbaff97 100644 --- a/parse_commands.go +++ b/parse_commands.go @@ -7,6 +7,7 @@ import ( "github.com/ParsePlatform/parse-cli/parsecli" "github.com/ParsePlatform/parse-cli/parsecmd" + "github.com/ParsePlatform/parse-cli/webhooks" "github.com/spf13/cobra" ) @@ -32,7 +33,7 @@ http://parse.com`, c.AddCommand(parsecmd.NewDeployCmd(e)) c.AddCommand(parsecmd.NewDevelopCmd(e)) c.AddCommand(parsecmd.NewDownloadCmd(e)) - c.AddCommand(NewFunctionHooksCmd(e)) + c.AddCommand(webhooks.NewFunctionHooksCmd(e)) c.AddCommand(parsecmd.NewGenerateCmd(e)) c.AddCommand(parsecmd.NewJsSdkCmd(e)) c.AddCommand(NewListCmd(e)) @@ -42,7 +43,7 @@ http://parse.com`, c.AddCommand(parsecmd.NewReleasesCmd(e)) c.AddCommand(parsecmd.NewRollbackCmd(e)) c.AddCommand(parsecmd.NewSymbolsCmd(e)) - c.AddCommand(NewTriggerHooksCmd(e)) + c.AddCommand(webhooks.NewTriggerHooksCmd(e)) c.AddCommand(NewUpdateCmd(e)) c.AddCommand(NewVersionCmd(e)) diff --git a/functions.go b/webhooks/functions.go similarity index 95% rename from functions.go rename to webhooks/functions.go index 4798c62..3ec9cd2 100644 --- a/functions.go +++ b/webhooks/functions.go @@ -1,4 +1,4 @@ -package main +package webhooks import ( "errors" @@ -19,6 +19,14 @@ type functionHook struct { Warning string `json:"warning,omitempty"` } +func getConfirmation(message string, e *parsecli.Env) bool { + fmt.Fprintf(e.Out, message) + var confirm string + fmt.Fscanf(e.In, "%s\n", &confirm) + lower := strings.ToLower(confirm) + return lower != "" && strings.HasPrefix(lower, "y") +} + func (f functionHook) String() string { if f.URL != "" { return fmt.Sprintf("Function name: %q, URL: %q", f.FunctionName, f.URL) diff --git a/functions_test.go b/webhooks/functions_test.go similarity index 99% rename from functions_test.go rename to webhooks/functions_test.go index c7f527c..8f9f839 100644 --- a/functions_test.go +++ b/webhooks/functions_test.go @@ -1,4 +1,4 @@ -package main +package webhooks import ( "encoding/json" diff --git a/webhooks/hooks.go b/webhooks/hooks.go new file mode 100644 index 0000000..00efdc3 --- /dev/null +++ b/webhooks/hooks.go @@ -0,0 +1,458 @@ +package webhooks + +import ( + "bufio" + "errors" + "fmt" + "io" + "io/ioutil" + "net/url" + "os" + "path" + "strings" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/stackerr" +) + +var ( + errInvalidFormat = errors.New( + ` +invalid format. +valid formats should look like: +[put|post],functionName,https_url +delete,functionName + +[put|post],className:triggerName,https_url +delete,className:triggerName +`) + + errPostToPut = errors.New( + `a hook with given name already exists: cannot create a new one. +`) + + errPutToPost = errors.New( + `a hook with the given name does not exist yet: cannot update the url. +`) + + errNotExist = errors.New( + `a hook with the given name does not exist. cannot delete it. + `) +) + +type hookOperation struct { + method string + function *functionHook + trigger *triggerHook +} + +func validateURL(urlStr string) error { + netURL, err := url.Parse(urlStr) + if err != nil { + return stackerr.Wrap(err) + } + + if netURL.Scheme != "https" { + return errors.New("Please enter a valid https url") + } + return nil +} + +func checkTriggerName(s string) error { + switch strings.ToLower(s) { + case "beforesave", "beforedelete", "aftersave", "afterdelete": + return nil + } + return stackerr.Newf( + `invalid trigger name: %v. + This is the list of valid trigger names: + beforeSave + afterSave + beforeDelete + afterDelete +`, + s, + ) +} + +type Hooks struct { + HooksStrict bool + BaseURL string + baseWebhookURL *url.URL +} + +func (h *Hooks) checkTriggerName(s string) error { + switch strings.ToLower(s) { + case "beforesave", "beforedelete", "aftersave", "afterdelete": + return nil + } + return stackerr.Newf( + `invalid trigger name: %v. + This is the list of valid trigger names: + beforeSave + afterSave + beforeDelete + afterDelete +`, + s, + ) +} + +func (h *Hooks) postOrPutHook( + e *parsecli.Env, + hooksOps []*hookOperation, + fields ...string, +) (bool, []*hookOperation, error) { + restOp := strings.ToUpper(fields[0]) + if restOp != "POST" && restOp != "PUT" { + return false, nil, stackerr.Wrap(errInvalidFormat) + } + + switch len(fields) { + case 3: + hooksOps = append(hooksOps, &hookOperation{ + method: restOp, + function: &functionHook{FunctionName: fields[1], URL: fields[2]}, + }) + return true, hooksOps, nil + + case 4: + if err := h.checkTriggerName(fields[2]); err != nil { + return false, nil, err + } + hooksOps = append(hooksOps, &hookOperation{ + method: restOp, + trigger: &triggerHook{ClassName: fields[1], TriggerName: fields[2], URL: fields[3]}, + }) + return true, hooksOps, nil + } + return false, nil, stackerr.Wrap(errInvalidFormat) + +} + +func (h *Hooks) deleteHook( + e *parsecli.Env, + hooksOps []*hookOperation, + fields ...string, +) (bool, []*hookOperation, error) { + restOp := strings.ToUpper(fields[0]) + if restOp != "DELETE" { + return false, nil, stackerr.Wrap(errInvalidFormat) + } + + switch len(fields) { + case 2: + hooksOps = append(hooksOps, &hookOperation{ + method: "DELETE", + function: &functionHook{FunctionName: fields[1]}, + }) + return true, hooksOps, nil + case 3: + if err := h.checkTriggerName(fields[2]); err != nil { + return false, nil, err + } + hooksOps = append(hooksOps, &hookOperation{ + method: "DELETE", + trigger: &triggerHook{ClassName: fields[1], TriggerName: fields[2]}, + }) + return true, hooksOps, nil + } + + return false, nil, stackerr.Wrap(errInvalidFormat) +} + +func (h *Hooks) appendHookOperation( + e *parsecli.Env, + fields []string, + hooks []*hookOperation, +) (bool, []*hookOperation, error) { + if len(fields) == 0 { + return false, hooks, nil + } + + switch strings.ToLower(fields[0]) { + case "post", "put": + return h.postOrPutHook(e, hooks, fields...) + + case "delete": + return h.deleteHook(e, hooks, fields...) + } + return false, nil, stackerr.Wrap(errInvalidFormat) +} + +func (h *Hooks) processHooksOperation(e *parsecli.Env, op string) ([]string, error) { + op = strings.TrimSpace(op) + if op == "" { + return nil, nil + } + + fields := strings.SplitN(op, ",", 3) + switch restOp := strings.ToLower(fields[0]); restOp { + case "post", "put": + if len(fields) < 3 { + return nil, stackerr.Wrap(errInvalidFormat) + } + case "delete": + if len(fields) != 2 { + return nil, stackerr.Wrap(errInvalidFormat) + } + subFields := strings.SplitN(fields[1], ":", 2) + if len(subFields) == 2 { + fields = append(fields, subFields[1]) + fields[1] = subFields[0] + } + return fields, nil + default: + return nil, stackerr.Wrap(errInvalidFormat) + } + + if h.baseWebhookURL != nil { + u, err := h.baseWebhookURL.Parse(fields[2]) + if err != nil { + return nil, stackerr.Wrap(err) + } + fields[2] = u.String() + } + + switch subFields := strings.SplitN(fields[1], ":", 2); len(subFields) { + case 1: + return fields, nil + case 2: + fields = append(fields, fields[2]) + fields[3] = fields[2] + fields[2] = subFields[1] + fields[1] = subFields[0] + return fields, nil + } + return nil, stackerr.Wrap(errInvalidFormat) +} + +func (h *Hooks) createHooksOperations( + e *parsecli.Env, + reader io.Reader, +) ([]*hookOperation, error) { + scanner := bufio.NewScanner(reader) + var ( + hooksOps []*hookOperation + added bool + ) + lineNum := 0 + for scanner.Scan() { + lineNum++ + fields, err := h.processHooksOperation(e, scanner.Text()) + if err != nil { + return nil, err + } + added, hooksOps, err = h.appendHookOperation(e, fields, hooksOps) + if err != nil { + return nil, err + } + if !added { + fmt.Fprintf(e.Out, "Ignoring line: %d\n", lineNum) + } + } + if err := scanner.Err(); err != nil { + return nil, stackerr.Wrap(err) + } + return hooksOps, nil +} + +func (h *Hooks) checkStrictMode(restOp string, exists bool) (string, bool, error) { + restOp = strings.ToUpper(restOp) + if !exists { + if restOp == "PUT" { + if h.HooksStrict { + return "", false, stackerr.Wrap(errPutToPost) + } + return "POST", true, nil + } + if restOp == "DELETE" { + if h.HooksStrict { + return "", false, stackerr.Wrap(errNotExist) + } + return "DELETE", true, nil + } + } else if restOp == "POST" { + if h.HooksStrict { + return "", false, stackerr.Wrap(errPostToPut) + } + return "PUT", true, nil + } + return restOp, false, nil +} + +func (h *Hooks) functionHookExists(e *parsecli.Env, name string) (bool, error) { + functionsURL, err := url.Parse(path.Join(defaultFunctionsURL, name)) + if err != nil { + return false, stackerr.Wrap(err) + } + var results struct { + Results []*functionHook `json:"results,omitempty"` + } + _, err = e.ParseAPIClient.Get(functionsURL, &results) + if err != nil { + if strings.Contains(err.Error(), "is defined") { + return false, nil + } + return false, stackerr.Wrap(err) + } + for _, result := range results.Results { + if result.URL != "" && result.FunctionName == name { + return true, nil + } + } + return false, nil +} + +func (h *Hooks) deployFunctionHook(e *parsecli.Env, op *hookOperation) error { + if op.function == nil { + return stackerr.New("cannot deploy nil function hook") + } + exists, err := h.functionHookExists(e, op.function.FunctionName) + if err != nil { + return err + } + + restOp, suppressed, err := h.checkStrictMode(op.method, exists) + if err != nil { + return err + } + + function := &functionHooksCmd{Function: op.function} + switch restOp { + case "POST": + return function.functionHooksCreate(e, nil) + case "PUT": + return function.functionHooksUpdate(e, nil) + case "DELETE": + if suppressed { + return nil + } + return function.functionHooksDelete(e, nil) + } + return stackerr.Wrap(errInvalidFormat) +} + +func (h *Hooks) triggerHookExists(e *parsecli.Env, className, triggerName string) (bool, error) { + triggersURL, err := url.Parse(path.Join(defaultTriggersURL, className, triggerName)) + if err != nil { + return false, stackerr.Wrap(err) + } + var results struct { + Results []*triggerHook `json:"results,omitempty"` + } + _, err = e.ParseAPIClient.Get(triggersURL, &results) + if err != nil { + if strings.Contains(err.Error(), "is defined") { + return false, nil + } + return false, stackerr.Wrap(err) + } + for _, result := range results.Results { + if result.URL != "" && result.ClassName == className && result.TriggerName == triggerName { + return true, nil + } + } + return false, nil +} + +func (h *Hooks) deployTriggerHook(e *parsecli.Env, op *hookOperation) error { + if op.trigger == nil { + return stackerr.New("cannot deploy nil trigger hook") + } + + exists, err := h.triggerHookExists(e, op.trigger.ClassName, op.trigger.TriggerName) + if err != nil { + return err + } + restOp, suppressed, err := h.checkStrictMode(op.method, exists) + if err != nil { + return err + } + + trigger := &triggerHooksCmd{Trigger: op.trigger, All: false} + switch restOp { + case "POST": + return trigger.triggerHooksCreate(e, nil) + case "PUT": + return trigger.triggerHooksUpdate(e, nil) + case "DELETE": + if suppressed { + return nil + } + return trigger.triggerHooksDelete(e, nil) + } + return stackerr.Wrap(errInvalidFormat) + +} + +func (h *Hooks) deployWebhooksConfig(e *parsecli.Env, hooksOps []*hookOperation) error { + for _, op := range hooksOps { + if op.function == nil && op.trigger == nil { + return stackerr.New("hook operation is neither a function, not a trigger.") + } + if op.function != nil && op.trigger != nil { + return stackerr.New("a hook cannot be both a function and a trigger.") + } + if op.function != nil { + if err := h.deployFunctionHook(e, op); err != nil { + return err + } + } else { + if err := h.deployTriggerHook(e, op); err != nil { + return err + } + } + fmt.Fprintln(e.Out) + } + return nil +} + +func (h *Hooks) parseBaseURL(e *parsecli.Env) error { + if h.BaseURL != "" { + u, err := url.Parse(h.BaseURL) + if err != nil { + fmt.Fprintln(e.Err, "Invalid base webhook url provided") + return stackerr.Wrap(err) + } + if u.Scheme != "https" { + return stackerr.New("Please provide a valid https url") + } + h.baseWebhookURL = u + } + return nil +} + +func (h *Hooks) HooksCmd(e *parsecli.Env, ctx *parsecli.Context, args []string) error { + if len(args) > 1 { + return fmt.Errorf("Invalid args: %v, only an optional hooks config file is expected.", args) + } + if err := h.parseBaseURL(e); err != nil { + return err + } + + reader := e.In + if len(args) == 1 { + file, err := os.Open(args[0]) + if err != nil { + return stackerr.Wrap(err) + } + reader = ioutil.NopCloser(file) + } else { + fmt.Fprintln(e.Out, "Since a webhooks config file was not provided reading from stdin.") + } + hooksOps, err := h.createHooksOperations(e, reader) + if err != nil { + return err + } + err = h.deployWebhooksConfig(e, hooksOps) + if err != nil { + fmt.Fprintln( + e.Out, + "Failed to deploy the webhooks config. Please try again...", + ) + return err + } + fmt.Fprintln(e.Out, "Successfully configured the given webhooks for the app.") + return nil +} diff --git a/webhooks/hooks_test.go b/webhooks/hooks_test.go new file mode 100644 index 0000000..cd4970e --- /dev/null +++ b/webhooks/hooks_test.go @@ -0,0 +1,436 @@ +package webhooks + +import ( + "io/ioutil" + "net/url" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/ensure" +) + +func TestCheckTriggerName(t *testing.T) { + t.Parallel() + + c := &Hooks{} + + ensure.Nil(t, c.checkTriggerName("beforeSave")) + ensure.Nil(t, c.checkTriggerName("afterSave")) + ensure.Nil(t, c.checkTriggerName("beforeDelete")) + ensure.Nil(t, c.checkTriggerName("afterDelete")) + + ensure.Nil(t, c.checkTriggerName("BeforeSAVE")) + ensure.Nil(t, c.checkTriggerName("AFTERsave")) + ensure.Nil(t, c.checkTriggerName("BeforeDELETE")) + ensure.Nil(t, c.checkTriggerName("AFTERdelete")) + + ensure.Err(t, c.checkTriggerName("invalid"), regexp.MustCompile("list of valid trigger names")) +} + +func TestPostOrPutHook(t *testing.T) { + t.Parallel() + h := parsecli.NewHarness(t) + defer h.Stop() + + c := &Hooks{} + + _, _, err := c.postOrPutHook(h.Env, nil, "other") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, _, err = c.postOrPutHook(h.Env, nil, "post", "1") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, _, err = c.postOrPutHook(h.Env, nil, "put", "1", "2", "3", "4") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + var hooksOps []*hookOperation + _, ops, err := c.postOrPutHook(h.Env, hooksOps, + "post", "call", "https://api.twilio.com/call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{ + FunctionName: "call", + URL: "https://api.twilio.com/call", + }, + ) + + _, ops, err = c.postOrPutHook(h.Env, hooksOps, + "put", "call", "https://api.twilio.com/call_1") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{ + FunctionName: "call", + URL: "https://api.twilio.com/call_1", + }, + ) + + _, ops, err = c.postOrPutHook(h.Env, hooksOps, "post", + "_User", "beforeSave", "https://api.twilio.com/message") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ + ClassName: "_User", + TriggerName: "beforeSave", + URL: "https://api.twilio.com/message", + }, + ) + + _, ops, err = c.postOrPutHook(h.Env, hooksOps, "put", + "_User", "beforeSave", "https://api.twilio.com/message_1") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ + ClassName: "_User", + TriggerName: "beforeSave", + URL: "https://api.twilio.com/message_1", + }, + ) + + _, ops, err = c.postOrPutHook(h.Env, hooksOps, "post", + "_User", "invalid", "https://other") + ensure.Err(t, err, regexp.MustCompile("invalid trigger")) +} + +func TestDeleteHook(t *testing.T) { + t.Parallel() + h := parsecli.NewHarness(t) + defer h.Stop() + + c := &Hooks{} + _, _, err := c.deleteHook(h.Env, nil, "delete") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, _, err = c.deleteHook(h.Env, nil, "invalid", "1") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, _, err = c.deleteHook(h.Env, nil, "delete", "1", "2") + ensure.Err(t, err, regexp.MustCompile("invalid trigger name")) + + var hooksOps []*hookOperation + _, ops, err := c.deleteHook(h.Env, hooksOps, "delete", "call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{FunctionName: "call"}, + ) + + _, ops, err = c.deleteHook(h.Env, hooksOps, "delete", "_User", "beforeSave") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ClassName: "_User", TriggerName: "beforeSave"}, + ) +} + +func TestProcessHookOperation(t *testing.T) { + t.Parallel() + + h := parsecli.NewHarness(t) + defer h.Stop() + + c := &Hooks{} + + ops, err := c.processHooksOperation(h.Env, "\n\t ") + ensure.Nil(t, err) + ensure.DeepEqual(t, len(ops), 0) + + _, err = c.processHooksOperation(h.Env, "invalid") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, err = c.processHooksOperation(h.Env, "delete,call,caller") + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + ops, err = c.processHooksOperation(h.Env, "delete,call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"delete", "call"}) + + ops, err = c.processHooksOperation(h.Env, "delete,_User:beforeSave") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"delete", "_User", "beforeSave"}) + + ops, err = c.processHooksOperation(h.Env, "post,call,https://twilio.com/call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) + + ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2"}) + + ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2,call_3") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) + + ops, err = c.processHooksOperation(h.Env, + "pUt,_User:afterDelete,https://twilio.com/message_1,message_2") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"pUt", "_User", "afterDelete", + "https://twilio.com/message_1,message_2"}) + + u, err := url.Parse("https://parse.com") + ensure.Nil(t, err) + c.baseWebhookURL = u + + ops, err = c.processHooksOperation(h.Env, "post,call,https://parse.com/call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"post", "call", "https://parse.com/call"}) + + ops, err = c.processHooksOperation(h.Env, "post,call,https://twilio.com/call") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) + + ops, err = c.processHooksOperation(h.Env, "put,call,/call_1,call_2,call_3") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"put", "call", "https://parse.com/call_1,call_2,call_3"}) + + ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2,call_3") + ensure.Nil(t, err) + ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) +} + +func TestAppendHookOperation(t *testing.T) { + t.Parallel() + + h := parsecli.NewHarness(t) + defer h.Stop() + + c := &Hooks{} + + var hooksOps []*hookOperation + _, ops, err := c.appendHookOperation(h.Env, nil, hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, hooksOps, ops) + + _, _, err = c.appendHookOperation(h.Env, []string{"invalid"}, nil) + ensure.Err(t, err, regexp.MustCompile("invalid format")) + + _, ops, err = c.appendHookOperation(h.Env, + []string{"post", "call", "https://twilio.com/call"}, hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{FunctionName: "call", URL: "https://twilio.com/call"}, + ) + + _, ops, err = c.appendHookOperation(h.Env, + []string{"put", "call", "https://twilio.com/call_1"}, hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].function, + functionHook{FunctionName: "call", URL: "https://twilio.com/call_1"}, + ) + + //random stuff + _, ops, err = c.appendHookOperation(h.Env, + []string{"posT", "_User", "afterDelete", "https://twilio.com/message"}, hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message"}, + ) + + _, ops, err = c.appendHookOperation(h.Env, + []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1"}, hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1"}, + ) + + // random stuff + _, ops, err = c.appendHookOperation(h.Env, + []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1,message_2"}, hooksOps) + ensure.Nil(t, err) + ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual( + t, + *ops[len(ops)-1].trigger, + triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1,message_2"}, + ) +} + +func TestCheckStrictMode(t *testing.T) { + t.Parallel() + + testCases := []struct { + restOp string + exists bool + strictMode bool + + op string + updated bool + err *regexp.Regexp + }{ + {exists: false, restOp: "put", strictMode: false, op: "POST", updated: true, err: nil}, + {exists: false, restOp: "post", strictMode: false, op: "POST", updated: false, err: nil}, + {exists: false, restOp: "delete", strictMode: false, op: "DELETE", updated: true, err: nil}, + {exists: true, restOp: "put", strictMode: false, op: "PUT", updated: false, err: nil}, + {exists: true, restOp: "post", strictMode: false, op: "PUT", updated: true, err: nil}, + {exists: true, restOp: "delete", strictMode: false, op: "DELETE", updated: false, err: nil}, + + {exists: false, restOp: "put", strictMode: true, err: regexp.MustCompile("does not exist")}, + {exists: false, restOp: "post", strictMode: true, op: "POST", updated: false, err: nil}, + {exists: false, restOp: "delete", strictMode: true, err: regexp.MustCompile("cannot delete")}, + {exists: true, restOp: "put", strictMode: true, op: "PUT", updated: false, err: nil}, + {exists: true, restOp: "post", strictMode: true, err: regexp.MustCompile("already exists")}, + {exists: true, restOp: "delete", strictMode: true, op: "DELETE", updated: false, err: nil}, + } + + c := &Hooks{} + for _, testCase := range testCases { + c.HooksStrict = testCase.strictMode + op, updated, err := c.checkStrictMode(testCase.restOp, testCase.exists) + if testCase.err == nil { + ensure.Nil(t, err) + ensure.DeepEqual(t, op, testCase.op) + ensure.DeepEqual(t, updated, testCase.updated) + } else { + ensure.Err(t, err, testCase.err) + } + } +} + +func TestCreateHookOperations(t *testing.T) { + t.Parallel() + h := parsecli.NewHarness(t) + h.MakeEmptyRoot() + defer h.Stop() + + hooksConfigFile := filepath.Join(h.Env.Root, "webhooks.csv") + err := ioutil.WriteFile( + hooksConfigFile, + []byte{}, + 0666, + ) + ensure.Nil(t, err) + + c := &Hooks{} + + f, err := os.Open(hooksConfigFile) + ensure.Nil(t, err) + ops, err := c.createHooksOperations(h.Env, f) + ensure.Nil(t, err) + ensure.DeepEqual(t, len(ops), 0) + ensure.Nil(t, f.Close()) + + err = ioutil.WriteFile( + hooksConfigFile, + []byte("\n"), + 0666, + ) + + f, err = os.Open(hooksConfigFile) + ensure.Nil(t, err) + ops, err = c.createHooksOperations(h.Env, f) + ensure.Nil(t, err) + ensure.DeepEqual(t, len(ops), 0) + ensure.DeepEqual(t, h.Out.String(), "Ignoring line: 1\n") + ensure.Nil(t, f.Close()) + h.Out.Reset() +} + +func TestFunctionHookExists(t *testing.T) { + t.Parallel() + h := newFunctionsHarness(t) + c := &Hooks{} + exists, err := c.functionHookExists(h.Env, "foo") + ensure.Nil(t, err) + ensure.True(t, exists) + + exists, err = c.functionHookExists(h.Env, "bar") + ensure.Nil(t, err) + ensure.False(t, exists) +} + +func TestDeployFunctionHooks(t *testing.T) { + t.Parallel() + h := newFunctionsHarness(t) + c := &Hooks{} + + err := c.deployFunctionHook( + h.Env, + &hookOperation{ + method: "post", + function: &functionHook{ + FunctionName: "foo", + URL: "https://api.example.com/foo", + }, + }, + ) + // not an error -> will be converted to put + ensure.Nil(t, err) +} + +func TestTriggerHookExists(t *testing.T) { + t.Parallel() + h := newTriggersHarness(t) + c := &Hooks{} + exists, err := c.triggerHookExists(h.Env, "foo", "beforeSave") + ensure.Nil(t, err) + ensure.True(t, exists) + + exists, err = c.triggerHookExists(h.Env, "bar", "other") + ensure.Nil(t, err) + ensure.False(t, exists) +} + +func TestDeployTriggerHooks(t *testing.T) { + t.Parallel() + h := newTriggersHarness(t) + c := &Hooks{} + + err := c.deployTriggerHook( + h.Env, + &hookOperation{ + method: "post", + trigger: &triggerHook{ + ClassName: "foo", + TriggerName: "beforeSave", + URL: "https://api.example.com/foo", + }, + }, + ) + // not an error -> will be converted to put + ensure.Nil(t, err) +} + +func TestParseBaseURL(t *testing.T) { + t.Parallel() + + h := parsecli.NewHarness(t) + defer h.Stop() + + c := &Hooks{} + c.BaseURL = "http://hello" + ensure.Err(t, c.parseBaseURL(h.Env), regexp.MustCompile("valid https url")) + + c.BaseURL = "https://hello" + ensure.Nil(t, c.parseBaseURL(h.Env)) + ensure.DeepEqual(t, c.baseWebhookURL.String(), c.BaseURL) +} diff --git a/triggers.go b/webhooks/triggers.go similarity index 99% rename from triggers.go rename to webhooks/triggers.go index db3d572..e015662 100644 --- a/triggers.go +++ b/webhooks/triggers.go @@ -1,4 +1,4 @@ -package main +package webhooks import ( "errors" diff --git a/triggers_test.go b/webhooks/triggers_test.go similarity index 99% rename from triggers_test.go rename to webhooks/triggers_test.go index 982476c..338ea68 100644 --- a/triggers_test.go +++ b/webhooks/triggers_test.go @@ -1,4 +1,4 @@ -package main +package webhooks import ( "encoding/json" From ac445ad856b276ad6e9b01454409971d730dcf8c Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 15 Oct 2015 15:07:59 +0000 Subject: [PATCH 070/106] [format] change webhooks format to json - csv is not future proof, especially if webhook format changes from logs it appears that no one has used 'configure hooks' yet. hence we need not be backward compatible --- configure.go | 2 +- webhooks/hooks.go | 200 ++++++----------------- webhooks/hooks_test.go | 357 ++++++++++++++++++----------------------- 3 files changed, 205 insertions(+), 354 deletions(-) diff --git a/configure.go b/configure.go index 8fb3d6d..7267eec 100644 --- a/configure.go +++ b/configure.go @@ -221,7 +221,7 @@ func NewConfigureCmd(e *parsecli.Env) *cobra.Command { hooksCmd := &cobra.Command{ Use: "hooks [filename]", Short: "Configure webhooks according to given config file", - Long: `Configure webhooks for the app based on the given configuration csv file. + Long: `Configure webhooks for the app based on the given configuration json file. For more details read: https://parse.com/docs/js/guide#command-line-webhooks `, Run: parsecli.RunWithArgsClient(e, c.hooks.HooksCmd), diff --git a/webhooks/hooks.go b/webhooks/hooks.go index 00efdc3..4f0daac 100644 --- a/webhooks/hooks.go +++ b/webhooks/hooks.go @@ -1,7 +1,7 @@ package webhooks import ( - "bufio" + "encoding/json" "errors" "fmt" "io" @@ -20,11 +20,16 @@ var ( ` invalid format. valid formats should look like: -[put|post],functionName,https_url -delete,functionName +{"hooks": [OPERATION]} -[put|post],className:triggerName,https_url -delete,className:triggerName +OPERATION -> +{"op": "put", "function": {"functionName": "name", "url": "https_url"}} +{"op": "post", "function": {"functionName": "name", "url": "https_url"}} +{"op": "delete", "function": {"functionName": "name"}} + +{"op": "put", "trigger": {"className": "cname", "triggerName": "tname", "url":"https_url"}} +{"op": "post", "trigger": {"className": "cname", "triggerName": "tname", "url":"https_url"}} +{"op": "delete", "trigger": {"className": "cname", "triggerName": "tname",}} `) errPostToPut = errors.New( @@ -41,9 +46,9 @@ delete,className:triggerName ) type hookOperation struct { - method string - function *functionHook - trigger *triggerHook + Method string `json:"op,omitempty"` + Function *functionHook `json:"function,omitempty"` + Trigger *triggerHook `json:"trigger,omitempty"` } func validateURL(urlStr string) error { @@ -98,162 +103,61 @@ func (h *Hooks) checkTriggerName(s string) error { ) } -func (h *Hooks) postOrPutHook( +func (h *Hooks) appendHookOperation( e *parsecli.Env, + hookOp *hookOperation, hooksOps []*hookOperation, - fields ...string, ) (bool, []*hookOperation, error) { - restOp := strings.ToUpper(fields[0]) - if restOp != "POST" && restOp != "PUT" { - return false, nil, stackerr.Wrap(errInvalidFormat) - } - - switch len(fields) { - case 3: - hooksOps = append(hooksOps, &hookOperation{ - method: restOp, - function: &functionHook{FunctionName: fields[1], URL: fields[2]}, - }) - return true, hooksOps, nil - - case 4: - if err := h.checkTriggerName(fields[2]); err != nil { - return false, nil, err - } - hooksOps = append(hooksOps, &hookOperation{ - method: restOp, - trigger: &triggerHook{ClassName: fields[1], TriggerName: fields[2], URL: fields[3]}, - }) - return true, hooksOps, nil + if hookOp == nil || (hookOp.Function == nil && hookOp.Trigger == nil) || + (hookOp.Function != nil && hookOp.Trigger != nil) { + return false, hooksOps, nil } - return false, nil, stackerr.Wrap(errInvalidFormat) - -} -func (h *Hooks) deleteHook( - e *parsecli.Env, - hooksOps []*hookOperation, - fields ...string, -) (bool, []*hookOperation, error) { - restOp := strings.ToUpper(fields[0]) - if restOp != "DELETE" { + method := strings.ToUpper(hookOp.Method) + if method != "POST" && method != "PUT" && method != "DELETE" { return false, nil, stackerr.Wrap(errInvalidFormat) } - switch len(fields) { - case 2: - hooksOps = append(hooksOps, &hookOperation{ - method: "DELETE", - function: &functionHook{FunctionName: fields[1]}, - }) - return true, hooksOps, nil - case 3: - if err := h.checkTriggerName(fields[2]); err != nil { + hookOp.Method = method + if hookOp.Trigger != nil { + if err := h.checkTriggerName(hookOp.Trigger.TriggerName); err != nil { return false, nil, err } - hooksOps = append(hooksOps, &hookOperation{ - method: "DELETE", - trigger: &triggerHook{ClassName: fields[1], TriggerName: fields[2]}, - }) - return true, hooksOps, nil - } - - return false, nil, stackerr.Wrap(errInvalidFormat) -} - -func (h *Hooks) appendHookOperation( - e *parsecli.Env, - fields []string, - hooks []*hookOperation, -) (bool, []*hookOperation, error) { - if len(fields) == 0 { - return false, hooks, nil - } - - switch strings.ToLower(fields[0]) { - case "post", "put": - return h.postOrPutHook(e, hooks, fields...) - - case "delete": - return h.deleteHook(e, hooks, fields...) - } - return false, nil, stackerr.Wrap(errInvalidFormat) -} - -func (h *Hooks) processHooksOperation(e *parsecli.Env, op string) ([]string, error) { - op = strings.TrimSpace(op) - if op == "" { - return nil, nil - } - - fields := strings.SplitN(op, ",", 3) - switch restOp := strings.ToLower(fields[0]); restOp { - case "post", "put": - if len(fields) < 3 { - return nil, stackerr.Wrap(errInvalidFormat) - } - case "delete": - if len(fields) != 2 { - return nil, stackerr.Wrap(errInvalidFormat) - } - subFields := strings.SplitN(fields[1], ":", 2) - if len(subFields) == 2 { - fields = append(fields, subFields[1]) - fields[1] = subFields[0] - } - return fields, nil - default: - return nil, stackerr.Wrap(errInvalidFormat) - } - - if h.baseWebhookURL != nil { - u, err := h.baseWebhookURL.Parse(fields[2]) - if err != nil { - return nil, stackerr.Wrap(err) - } - fields[2] = u.String() } - switch subFields := strings.SplitN(fields[1], ":", 2); len(subFields) { - case 1: - return fields, nil - case 2: - fields = append(fields, fields[2]) - fields[3] = fields[2] - fields[2] = subFields[1] - fields[1] = subFields[0] - return fields, nil - } - return nil, stackerr.Wrap(errInvalidFormat) + hooksOps = append(hooksOps, hookOp) + return true, hooksOps, nil } func (h *Hooks) createHooksOperations( e *parsecli.Env, reader io.Reader, ) ([]*hookOperation, error) { - scanner := bufio.NewScanner(reader) + var input struct { + HooksOps []*hookOperation `json:"hooks,omitempty"` + } + err := json.NewDecoder(ioutil.NopCloser(reader)).Decode(&input) + if err != nil { + return nil, stackerr.Wrap(err) + } + var ( hooksOps []*hookOperation added bool ) - lineNum := 0 - for scanner.Scan() { - lineNum++ - fields, err := h.processHooksOperation(e, scanner.Text()) - if err != nil { - return nil, err - } - added, hooksOps, err = h.appendHookOperation(e, fields, hooksOps) + for _, hookOp := range input.HooksOps { + added, hooksOps, err = h.appendHookOperation(e, hookOp, hooksOps) if err != nil { return nil, err } if !added { - fmt.Fprintf(e.Out, "Ignoring line: %d\n", lineNum) + op, err := json.MarshalIndent(hookOp, "", " ") + if err == nil { + fmt.Fprintf(e.Out, "Ignoring hook operation: \n%s\n", op) + } } } - if err := scanner.Err(); err != nil { - return nil, stackerr.Wrap(err) - } + return hooksOps, nil } @@ -305,20 +209,20 @@ func (h *Hooks) functionHookExists(e *parsecli.Env, name string) (bool, error) { } func (h *Hooks) deployFunctionHook(e *parsecli.Env, op *hookOperation) error { - if op.function == nil { + if op.Function == nil { return stackerr.New("cannot deploy nil function hook") } - exists, err := h.functionHookExists(e, op.function.FunctionName) + exists, err := h.functionHookExists(e, op.Function.FunctionName) if err != nil { return err } - restOp, suppressed, err := h.checkStrictMode(op.method, exists) + restOp, suppressed, err := h.checkStrictMode(op.Method, exists) if err != nil { return err } - function := &functionHooksCmd{Function: op.function} + function := &functionHooksCmd{Function: op.Function} switch restOp { case "POST": return function.functionHooksCreate(e, nil) @@ -357,20 +261,20 @@ func (h *Hooks) triggerHookExists(e *parsecli.Env, className, triggerName string } func (h *Hooks) deployTriggerHook(e *parsecli.Env, op *hookOperation) error { - if op.trigger == nil { + if op.Trigger == nil { return stackerr.New("cannot deploy nil trigger hook") } - exists, err := h.triggerHookExists(e, op.trigger.ClassName, op.trigger.TriggerName) + exists, err := h.triggerHookExists(e, op.Trigger.ClassName, op.Trigger.TriggerName) if err != nil { return err } - restOp, suppressed, err := h.checkStrictMode(op.method, exists) + restOp, suppressed, err := h.checkStrictMode(op.Method, exists) if err != nil { return err } - trigger := &triggerHooksCmd{Trigger: op.trigger, All: false} + trigger := &triggerHooksCmd{Trigger: op.Trigger, All: false} switch restOp { case "POST": return trigger.triggerHooksCreate(e, nil) @@ -388,13 +292,13 @@ func (h *Hooks) deployTriggerHook(e *parsecli.Env, op *hookOperation) error { func (h *Hooks) deployWebhooksConfig(e *parsecli.Env, hooksOps []*hookOperation) error { for _, op := range hooksOps { - if op.function == nil && op.trigger == nil { + if op.Function == nil && op.Trigger == nil { return stackerr.New("hook operation is neither a function, not a trigger.") } - if op.function != nil && op.trigger != nil { + if op.Function != nil && op.Trigger != nil { return stackerr.New("a hook cannot be both a function and a trigger.") } - if op.function != nil { + if op.Function != nil { if err := h.deployFunctionHook(e, op); err != nil { return err } @@ -437,7 +341,7 @@ func (h *Hooks) HooksCmd(e *parsecli.Env, ctx *parsecli.Context, args []string) if err != nil { return stackerr.Wrap(err) } - reader = ioutil.NopCloser(file) + reader = file } else { fmt.Fprintln(e.Out, "Since a webhooks config file was not provided reading from stdin.") } diff --git a/webhooks/hooks_test.go b/webhooks/hooks_test.go index cd4970e..b84e83c 100644 --- a/webhooks/hooks_test.go +++ b/webhooks/hooks_test.go @@ -2,7 +2,6 @@ package webhooks import ( "io/ioutil" - "net/url" "os" "path/filepath" "regexp" @@ -30,182 +29,6 @@ func TestCheckTriggerName(t *testing.T) { ensure.Err(t, c.checkTriggerName("invalid"), regexp.MustCompile("list of valid trigger names")) } -func TestPostOrPutHook(t *testing.T) { - t.Parallel() - h := parsecli.NewHarness(t) - defer h.Stop() - - c := &Hooks{} - - _, _, err := c.postOrPutHook(h.Env, nil, "other") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, _, err = c.postOrPutHook(h.Env, nil, "post", "1") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, _, err = c.postOrPutHook(h.Env, nil, "put", "1", "2", "3", "4") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - var hooksOps []*hookOperation - _, ops, err := c.postOrPutHook(h.Env, hooksOps, - "post", "call", "https://api.twilio.com/call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{ - FunctionName: "call", - URL: "https://api.twilio.com/call", - }, - ) - - _, ops, err = c.postOrPutHook(h.Env, hooksOps, - "put", "call", "https://api.twilio.com/call_1") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{ - FunctionName: "call", - URL: "https://api.twilio.com/call_1", - }, - ) - - _, ops, err = c.postOrPutHook(h.Env, hooksOps, "post", - "_User", "beforeSave", "https://api.twilio.com/message") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ - ClassName: "_User", - TriggerName: "beforeSave", - URL: "https://api.twilio.com/message", - }, - ) - - _, ops, err = c.postOrPutHook(h.Env, hooksOps, "put", - "_User", "beforeSave", "https://api.twilio.com/message_1") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ - ClassName: "_User", - TriggerName: "beforeSave", - URL: "https://api.twilio.com/message_1", - }, - ) - - _, ops, err = c.postOrPutHook(h.Env, hooksOps, "post", - "_User", "invalid", "https://other") - ensure.Err(t, err, regexp.MustCompile("invalid trigger")) -} - -func TestDeleteHook(t *testing.T) { - t.Parallel() - h := parsecli.NewHarness(t) - defer h.Stop() - - c := &Hooks{} - _, _, err := c.deleteHook(h.Env, nil, "delete") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, _, err = c.deleteHook(h.Env, nil, "invalid", "1") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, _, err = c.deleteHook(h.Env, nil, "delete", "1", "2") - ensure.Err(t, err, regexp.MustCompile("invalid trigger name")) - - var hooksOps []*hookOperation - _, ops, err := c.deleteHook(h.Env, hooksOps, "delete", "call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") - ensure.DeepEqual( - t, - *ops[len(ops)-1].function, - functionHook{FunctionName: "call"}, - ) - - _, ops, err = c.deleteHook(h.Env, hooksOps, "delete", "_User", "beforeSave") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "DELETE") - ensure.DeepEqual( - t, - *ops[len(ops)-1].trigger, - triggerHook{ClassName: "_User", TriggerName: "beforeSave"}, - ) -} - -func TestProcessHookOperation(t *testing.T) { - t.Parallel() - - h := parsecli.NewHarness(t) - defer h.Stop() - - c := &Hooks{} - - ops, err := c.processHooksOperation(h.Env, "\n\t ") - ensure.Nil(t, err) - ensure.DeepEqual(t, len(ops), 0) - - _, err = c.processHooksOperation(h.Env, "invalid") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - _, err = c.processHooksOperation(h.Env, "delete,call,caller") - ensure.Err(t, err, regexp.MustCompile("invalid format")) - - ops, err = c.processHooksOperation(h.Env, "delete,call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"delete", "call"}) - - ops, err = c.processHooksOperation(h.Env, "delete,_User:beforeSave") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"delete", "_User", "beforeSave"}) - - ops, err = c.processHooksOperation(h.Env, "post,call,https://twilio.com/call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) - - ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2"}) - - ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2,call_3") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) - - ops, err = c.processHooksOperation(h.Env, - "pUt,_User:afterDelete,https://twilio.com/message_1,message_2") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"pUt", "_User", "afterDelete", - "https://twilio.com/message_1,message_2"}) - - u, err := url.Parse("https://parse.com") - ensure.Nil(t, err) - c.baseWebhookURL = u - - ops, err = c.processHooksOperation(h.Env, "post,call,https://parse.com/call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"post", "call", "https://parse.com/call"}) - - ops, err = c.processHooksOperation(h.Env, "post,call,https://twilio.com/call") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"post", "call", "https://twilio.com/call"}) - - ops, err = c.processHooksOperation(h.Env, "put,call,/call_1,call_2,call_3") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"put", "call", "https://parse.com/call_1,call_2,call_3"}) - - ops, err = c.processHooksOperation(h.Env, "put,call,https://twilio.com/call_1,call_2,call_3") - ensure.Nil(t, err) - ensure.DeepEqual(t, ops, []string{"put", "call", "https://twilio.com/call_1,call_2,call_3"}) -} - func TestAppendHookOperation(t *testing.T) { t.Parallel() @@ -219,58 +42,102 @@ func TestAppendHookOperation(t *testing.T) { ensure.Nil(t, err) ensure.DeepEqual(t, hooksOps, ops) - _, _, err = c.appendHookOperation(h.Env, []string{"invalid"}, nil) - ensure.Err(t, err, regexp.MustCompile("invalid format")) + _, ops, err = c.appendHookOperation(h.Env, &hookOperation{}, nil) + ensure.Nil(t, err) + ensure.DeepEqual(t, hooksOps, ops) _, ops, err = c.appendHookOperation(h.Env, - []string{"post", "call", "https://twilio.com/call"}, hooksOps) + &hookOperation{ + Method: "post", + Function: &functionHook{ + FunctionName: "call", + URL: "https://twilio.com/call", + }, + }, + hooksOps, + ) ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual(t, ops[len(ops)-1].Method, "POST") ensure.DeepEqual( t, - *ops[len(ops)-1].function, + *ops[len(ops)-1].Function, functionHook{FunctionName: "call", URL: "https://twilio.com/call"}, ) _, ops, err = c.appendHookOperation(h.Env, - []string{"put", "call", "https://twilio.com/call_1"}, hooksOps) + &hookOperation{ + Method: "put", + Function: &functionHook{ + FunctionName: "call", + URL: "https://twilio.com/call_1", + }, + }, + hooksOps, + ) ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual(t, ops[len(ops)-1].Method, "PUT") ensure.DeepEqual( t, - *ops[len(ops)-1].function, + *ops[len(ops)-1].Function, functionHook{FunctionName: "call", URL: "https://twilio.com/call_1"}, ) //random stuff _, ops, err = c.appendHookOperation(h.Env, - []string{"posT", "_User", "afterDelete", "https://twilio.com/message"}, hooksOps) + &hookOperation{ + Method: "posT", + Trigger: &triggerHook{ + ClassName: "_User", + TriggerName: "afterDelete", + URL: "https://twilio.com/message", + }, + }, + hooksOps, + ) ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "POST") + ensure.DeepEqual(t, ops[len(ops)-1].Method, "POST") ensure.DeepEqual( t, - *ops[len(ops)-1].trigger, + *ops[len(ops)-1].Trigger, triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message"}, ) _, ops, err = c.appendHookOperation(h.Env, - []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1"}, hooksOps) + &hookOperation{ + Method: "pUt", + Trigger: &triggerHook{ + ClassName: "_User", + TriggerName: "afterDelete", + URL: "https://twilio.com/message_1", + }, + }, + hooksOps, + ) ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual(t, ops[len(ops)-1].Method, "PUT") ensure.DeepEqual( t, - *ops[len(ops)-1].trigger, + *ops[len(ops)-1].Trigger, triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1"}, ) // random stuff _, ops, err = c.appendHookOperation(h.Env, - []string{"pUt", "_User", "afterDelete", "https://twilio.com/message_1,message_2"}, hooksOps) + &hookOperation{ + Method: "pUt", + Trigger: &triggerHook{ + ClassName: "_User", + TriggerName: "afterDelete", + URL: "https://twilio.com/message_1,message_2", + }, + }, + hooksOps, + ) ensure.Nil(t, err) - ensure.DeepEqual(t, ops[len(ops)-1].method, "PUT") + ensure.DeepEqual(t, ops[len(ops)-1].Method, "PUT") ensure.DeepEqual( t, - *ops[len(ops)-1].trigger, + *ops[len(ops)-1].Trigger, triggerHook{ClassName: "_User", TriggerName: "afterDelete", URL: "https://twilio.com/message_1,message_2"}, ) } @@ -322,16 +189,16 @@ func TestCreateHookOperations(t *testing.T) { h.MakeEmptyRoot() defer h.Stop() - hooksConfigFile := filepath.Join(h.Env.Root, "webhooks.csv") + c := &Hooks{} + + hooksConfigFile := filepath.Join(h.Env.Root, "webhooks.json") err := ioutil.WriteFile( hooksConfigFile, - []byte{}, + []byte("{}"), 0666, ) ensure.Nil(t, err) - c := &Hooks{} - f, err := os.Open(hooksConfigFile) ensure.Nil(t, err) ops, err := c.createHooksOperations(h.Env, f) @@ -341,18 +208,98 @@ func TestCreateHookOperations(t *testing.T) { err = ioutil.WriteFile( hooksConfigFile, - []byte("\n"), + []byte("{1}"), 0666, ) + ensure.Nil(t, err) f, err = os.Open(hooksConfigFile) ensure.Nil(t, err) ops, err = c.createHooksOperations(h.Env, f) - ensure.Nil(t, err) + ensure.Err(t, err, regexp.MustCompile("invalid")) ensure.DeepEqual(t, len(ops), 0) - ensure.DeepEqual(t, h.Out.String(), "Ignoring line: 1\n") + ensure.Nil(t, f.Close()) + + err = ioutil.WriteFile( + hooksConfigFile, + []byte(`{ + "hooks": [ + { + "op": "put", + "function": { + "functionName": "message", + "url": "https://api.twilio.com/message" + } + } + ]}`), + 0666, + ) + + f, err = os.Open(hooksConfigFile) + ensure.Nil(t, err) + ops, err = c.createHooksOperations(h.Env, f) + ensure.Nil(t, err) + ensure.DeepEqual(t, len(ops), 1) + ensure.DeepEqual(t, + *ops[0].Function, + functionHook{FunctionName: "message", URL: "https://api.twilio.com/message"}, + ) + ensure.DeepEqual(t, ops[0].Method, "PUT") ensure.Nil(t, f.Close()) h.Out.Reset() + + err = ioutil.WriteFile( + hooksConfigFile, + []byte(`{ + "hooks": [ + { + "op": "post", + "function": { + "functionName": "message", + "url": "https://api.twilio.com/message" + } + }, + { + "op": "put", + "function": { + "functionName": "message", + "url": "https://api.twilio.com/message_1" + } + }, + { + "op": "post" + } + ]}`), + 0666, + ) + + f, err = os.Open(hooksConfigFile) + ensure.Nil(t, err) + ops, err = c.createHooksOperations(h.Env, f) + ensure.Nil(t, err) + ensure.DeepEqual(t, len(ops), 2) + ensure.DeepEqual(t, + *ops[0].Function, + functionHook{FunctionName: "message", URL: "https://api.twilio.com/message"}, + ) + ensure.DeepEqual(t, ops[0].Method, "POST") + + ensure.DeepEqual(t, + *ops[1].Function, + functionHook{FunctionName: "message", URL: "https://api.twilio.com/message_1"}, + ) + ensure.DeepEqual(t, + h.Out.String(), + `Ignoring hook operation: +{ + "op": "post" +} +`, + ) + ensure.DeepEqual(t, ops[1].Method, "PUT") + ensure.Nil(t, f.Close()) + h.Out.Reset() + } func TestFunctionHookExists(t *testing.T) { @@ -376,8 +323,8 @@ func TestDeployFunctionHooks(t *testing.T) { err := c.deployFunctionHook( h.Env, &hookOperation{ - method: "post", - function: &functionHook{ + Method: "post", + Function: &functionHook{ FunctionName: "foo", URL: "https://api.example.com/foo", }, @@ -408,8 +355,8 @@ func TestDeployTriggerHooks(t *testing.T) { err := c.deployTriggerHook( h.Env, &hookOperation{ - method: "post", - trigger: &triggerHook{ + Method: "post", + Trigger: &triggerHook{ ClassName: "foo", TriggerName: "beforeSave", URL: "https://api.example.com/foo", From 0fbabf9d7569bac0be7f169572bc1790755226a7 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 15 Oct 2015 20:02:14 +0000 Subject: [PATCH 071/106] [hooks] fix invalid json in hooks error --- webhooks/hooks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webhooks/hooks.go b/webhooks/hooks.go index 4f0daac..7d44969 100644 --- a/webhooks/hooks.go +++ b/webhooks/hooks.go @@ -29,7 +29,7 @@ OPERATION -> {"op": "put", "trigger": {"className": "cname", "triggerName": "tname", "url":"https_url"}} {"op": "post", "trigger": {"className": "cname", "triggerName": "tname", "url":"https_url"}} -{"op": "delete", "trigger": {"className": "cname", "triggerName": "tname",}} +{"op": "delete", "trigger": {"className": "cname", "triggerName": "tname"}} `) errPostToPut = errors.New( From 1d67e225a33d7cd55f89152c9b194bce9108c9ef Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 16 Oct 2015 04:50:02 +0000 Subject: [PATCH 072/106] [hooks] fix error check for trigger hook exists the error check for trigger hook was incorrect, causing it to fail when triggers are not defined --- webhooks/hooks.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webhooks/hooks.go b/webhooks/hooks.go index 7d44969..4ee050a 100644 --- a/webhooks/hooks.go +++ b/webhooks/hooks.go @@ -247,7 +247,7 @@ func (h *Hooks) triggerHookExists(e *parsecli.Env, className, triggerName string } _, err = e.ParseAPIClient.Get(triggersURL, &results) if err != nil { - if strings.Contains(err.Error(), "is defined") { + if strings.Contains(err.Error(), "defined for") { return false, nil } return false, stackerr.Wrap(err) From b0bc76b67b00342738d6655283e55c1891ae3ee4 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 16 Oct 2015 04:53:48 +0000 Subject: [PATCH 073/106] [hooks] provide helpful message for incorrect op * provide helpful message for incorrect op case * fix breaking test --- webhooks/hooks.go | 4 ++++ webhooks/triggers_test.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/webhooks/hooks.go b/webhooks/hooks.go index 4ee050a..80ce450 100644 --- a/webhooks/hooks.go +++ b/webhooks/hooks.go @@ -115,6 +115,10 @@ func (h *Hooks) appendHookOperation( method := strings.ToUpper(hookOp.Method) if method != "POST" && method != "PUT" && method != "DELETE" { + op, err := json.MarshalIndent(hookOp, "", " ") + if err == nil { + fmt.Fprintf(e.Out, "Please fix the incorrect operation in:\n%s\n", op) + } return false, nil, stackerr.Wrap(errInvalidFormat) } diff --git a/webhooks/triggers_test.go b/webhooks/triggers_test.go index 338ea68..4ddcdab 100644 --- a/webhooks/triggers_test.go +++ b/webhooks/triggers_test.go @@ -49,7 +49,7 @@ func newTriggersHarness(t testing.TB) *parsecli.Harness { } } else { return &http.Response{StatusCode: http.StatusBadRequest}, - errors.New("no such trigger is defined") + errors.New("no such trigger is defined for class") } case "POST": ensure.DeepEqual(t, r.URL.Path, defaultTriggersURL) From 754625b9aa913a65a6a7ae1c59e52a8c874e47dc Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sat, 17 Oct 2015 15:09:29 +0000 Subject: [PATCH 074/106] [hooks] add back base url functionality add ability to pass base url to during hooks registartion --- webhooks/hooks.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/webhooks/hooks.go b/webhooks/hooks.go index 80ce450..eed3f41 100644 --- a/webhooks/hooks.go +++ b/webhooks/hooks.go @@ -127,8 +127,20 @@ func (h *Hooks) appendHookOperation( if err := h.checkTriggerName(hookOp.Trigger.TriggerName); err != nil { return false, nil, err } + if hookOp.Trigger.URL != "" && h.baseWebhookURL != nil { + u, err := url.Parse(hookOp.Trigger.URL) + if err == nil { + hookOp.Trigger.URL = h.baseWebhookURL.ResolveReference(u).String() + } + } + } else { + if hookOp.Function.URL != "" && h.baseWebhookURL != nil { + u, err := url.Parse(hookOp.Function.URL) + if err == nil { + hookOp.Function.URL = h.baseWebhookURL.ResolveReference(u).String() + } + } } - hooksOps = append(hooksOps, hookOp) return true, hooksOps, nil } From de3d77177ff9d0669b5e7c460f62d6da91b86089 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 18 Oct 2015 18:15:36 +0000 Subject: [PATCH 075/106] [links] fix links to cli & cloudcode - recently I moved cloudcode to its own top level in parse.com/docs accordingly fix the links in cli docs --- README.md | 2 +- configure.go | 2 +- parsecli/login.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4843b96..2c9d022 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Parse Command Line Tool The `Parse Command Line Tool` allows you to interact with your Cloud Code from the terminal. -For more information please visit [Parse](https://parse.com/docs/js/guide#command-line). +For more information please visit [Parse](https://parse.com/docs/cloudcode/guide#command-line). Overview -------- diff --git a/configure.go b/configure.go index 7267eec..3e6815e 100644 --- a/configure.go +++ b/configure.go @@ -222,7 +222,7 @@ func NewConfigureCmd(e *parsecli.Env) *cobra.Command { Use: "hooks [filename]", Short: "Configure webhooks according to given config file", Long: `Configure webhooks for the app based on the given configuration json file. -For more details read: https://parse.com/docs/js/guide#command-line-webhooks +For more details read: https://parse.com/docs/cloudcode/guide#command-line-webhooks `, Run: parsecli.RunWithArgsClient(e, c.hooks.HooksCmd), Aliases: []string{"webhooks"}, diff --git a/parsecli/login.go b/parsecli/login.go index 3453a2d..79b1660 100644 --- a/parsecli/login.go +++ b/parsecli/login.go @@ -269,7 +269,7 @@ To save time logging in, you should create an account key. fmt.Fprintln( e.Out, `Type "parse configure accountkey" to create a new account key. -Read more at: https://parse.com/docs/js/guide#command-line-account-keys +Read more at: https://parse.com/docs/cloudcode/guide#command-line-account-keys Please login to Parse using your email and password.`, ) From 0ecfe8636d1cb4c342f1ae505cd395af8f1f5d58 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 18 Oct 2015 19:09:32 +0000 Subject: [PATCH 076/106] [develop] let develop deploy to default app - let `parse develop` deploy to default app, if no app is provided in arguments. however, ask for confirmation in that case. --- parsecli/runners.go | 39 ++++++++++++++++++++++++++++++--------- parsecmd/develop.go | 2 +- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/parsecli/runners.go b/parsecli/runners.go index a644caa..84247c3 100644 --- a/parsecli/runners.go +++ b/parsecli/runners.go @@ -2,6 +2,7 @@ package parsecli import ( "fmt" + "strings" "github.com/spf13/cobra" ) @@ -62,29 +63,49 @@ func RunWithClient(e *Env, f func(*Env, *Context) error) cobraRun { } } -// RunWithAppClient wraps a run function that should get an app -func RunWithAppClient(e *Env, f func(*Env, *Context) error) cobraRun { +// RunWithClientConfirm wraps a run function that takes an app name +// or asks for confirmation to use the default app name instead +func RunWithClientConfirm(e *Env, f func(*Env, *Context) error) cobraRun { return func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - fmt.Fprintf(e.Err, "please provide an app name\n\n") - cmd.Help() - e.Exit(1) - } + app := DefaultKey if len(args) > 1 { fmt.Fprintf( e.Err, - "unexpected arguments, only an app name is expected:%+v\n\n", + "unexpected arguments, only an optional app name is expected:%+v\n\n", args, ) cmd.Help() e.Exit(1) } - app := args[0] + if len(args) == 1 { + app = args[0] + } cl, err := newContext(e, app) if err != nil { fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) } + + if app == DefaultKey { + fmt.Fprintf( + e.Out, + `Did not provide app name as an argument to the command. +Please enter an app name to execute this command on +or press ENTER to use the default app %q: `, + cl.Config.GetDefaultApp(), + ) + var appName string + fmt.Fscanf(e.In, "%s\n", &appName) + appName = strings.TrimSpace(appName) + if appName != "" { + cl, err = newContext(e, appName) + if err != nil { + fmt.Fprintln(e.Err, ErrorString(e, err)) + e.Exit(1) + } + } + } + if err := f(e, cl); err != nil { fmt.Fprintln(e.Err, ErrorString(e, err)) e.Exit(1) diff --git a/parsecmd/develop.go b/parsecmd/develop.go index 457b961..4293df8 100644 --- a/parsecmd/develop.go +++ b/parsecmd/develop.go @@ -131,7 +131,7 @@ func NewDevelopCmd(e *parsecli.Env) *cobra.Command { `This will also monitor the parse INFO log for any new log messages and write ` + `out updates to the terminal. This requires an app to be provided, to ` + `avoid running develop on production apps accidently.`, - Run: parsecli.RunWithAppClient(e, d.run), + Run: parsecli.RunWithClientConfirm(e, d.run), } cmd.Flags().DurationVarP(&d.deployInterval, "interval", "i", d.deployInterval, "Number of seconds between deploys.") cmd.Flags().BoolVarP(&d.mustFetch, "fetch", "f", d.mustFetch, "Always fetch previous deployment info from server") From b6c056599ef97c548234f3bf4ceac68c41a8fc13 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 18 Oct 2015 22:44:51 +0000 Subject: [PATCH 077/106] [parse-cli] bump version to 2.2.9 --- parsecli/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsecli/main.go b/parsecli/main.go index 5bb50c2..e277596 100644 --- a/parsecli/main.go +++ b/parsecli/main.go @@ -21,7 +21,7 @@ import ( ) const ( - Version = "2.2.8" + Version = "2.2.9" CloudDir = "cloud" HostingDir = "public" DefaultBaseURL = "https://api.parse.com/1/" From c8e4ab8ff96da937d66a9bf842a88eef433fe058 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Tue, 20 Oct 2015 14:48:35 +0000 Subject: [PATCH 078/106] [login] do not panic on nil response - this was identified in https://github.com/ParsePlatform/parse-cli/issues/40 we should not be panicing on network errors --- parsecli/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsecli/login.go b/parsecli/login.go index 79b1660..49b1c5d 100644 --- a/parsecli/login.go +++ b/parsecli/login.go @@ -219,7 +219,7 @@ func (l *Login) AuthToken(e *Env, token string) (string, error) { Email string `json:"email"` }{} if response, err := e.ParseAPIClient.Do(req, nil, res); err != nil { - if response.StatusCode == http.StatusUnauthorized { + if response != nil && response.StatusCode == http.StatusUnauthorized { return "", stackerr.Newf(tokenErrMsgf, Last4(token), keysURL) } return "", stackerr.Wrap(err) From b0b2ed5876b4727d769d6dbd23140f9f9e1cce22 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Sun, 4 Oct 2015 23:03:06 +0000 Subject: [PATCH 079/106] add native support for server code on heroku - adds native support for running server code on heroku - also some reorganization around webhook registration --- add.go | 23 +++- heroku_commands.go | 64 ++++++++++ herokucmd/add.go | 215 +++++++++++++++++++++++++++++++ herokucmd/deploy.go | 142 +++++++++++++++++++++ herokucmd/download.go | 41 ++++++ herokucmd/git.go | 175 ++++++++++++++++++++++++++ herokucmd/link.go | 225 +++++++++++++++++++++++++++++++++ herokucmd/logs.go | 59 +++++++++ herokucmd/new.go | 154 +++++++++++++++++++++++ herokucmd/releases.go | 55 ++++++++ herokucmd/rollback.go | 62 +++++++++ herokucmd/unzip.go | 50 ++++++++ list.go | 2 +- main.go | 12 +- new.go | 87 ++++++++++--- new_test.go | 11 +- parsecli/apps.go | 4 +- parsecli/apps_test.go | 2 +- parsecli/client.go | 5 + parsecli/config.go | 23 +++- parsecli/context.go | 18 ++- parsecli/heroku_config.go | 258 ++++++++++++++++++++++++++++++++++++++ parsecli/login.go | 65 ++++++---- parsecli/login_test.go | 10 +- parsecli/main.go | 26 ++-- parsecli/parse_config.go | 2 +- 26 files changed, 1722 insertions(+), 68 deletions(-) create mode 100644 heroku_commands.go create mode 100644 herokucmd/add.go create mode 100644 herokucmd/deploy.go create mode 100644 herokucmd/download.go create mode 100644 herokucmd/git.go create mode 100644 herokucmd/link.go create mode 100644 herokucmd/logs.go create mode 100644 herokucmd/new.go create mode 100644 herokucmd/releases.go create mode 100644 herokucmd/rollback.go create mode 100644 herokucmd/unzip.go create mode 100644 parsecli/heroku_config.go diff --git a/add.go b/add.go index e5c61d6..3e8e587 100644 --- a/add.go +++ b/add.go @@ -1,6 +1,7 @@ package main import ( + "github.com/ParsePlatform/parse-cli/herokucmd" "github.com/ParsePlatform/parse-cli/parsecli" "github.com/ParsePlatform/parse-cli/parsecmd" "github.com/facebookgo/stackerr" @@ -26,6 +27,12 @@ func (a *addCmd) addSelectedApp( return stackerr.New("invalid parse app config passed.") } return parsecmd.AddSelectedParseApp(name, parseAppConfig, args, a.MakeDefault, a.verbose, e) + case parsecli.HerokuFormat: + herokuAppConfig, ok := appConfig.(*parsecli.HerokuAppConfig) + if !ok { + return stackerr.New("invalid heroku app config passed.") + } + return herokucmd.AddSelectedHerokuApp(name, herokuAppConfig, args, a.MakeDefault, a.verbose, e) } return stackerr.Newf("Unknown project type: %d.", e.Type) @@ -54,7 +61,7 @@ func (a *addCmd) selectApp(e *parsecli.Env, appName string) (*parsecli.App, erro func (a *addCmd) run(e *parsecli.Env, args []string) error { - if err := a.apps.Login.AuthUser(e); err != nil { + if err := a.apps.Login.AuthUser(e, false); err != nil { return err } var appName string @@ -69,7 +76,19 @@ func (a *addCmd) run(e *parsecli.Env, args []string) error { if err != nil { return err } - appConfig := parsecmd.GetParseAppConfig(app) + + var appConfig parsecli.AppConfig + switch e.Type { + case parsecli.LegacyParseFormat, parsecli.ParseFormat: + appConfig = parsecmd.GetParseAppConfig(app) + + case parsecli.HerokuFormat: + _, appConfig, err = herokucmd.GetLinkedHerokuAppConfig(app, e) + if err != nil { + return err + } + } + return a.addSelectedApp(app.Name, appConfig, args, e) } diff --git a/heroku_commands.go b/heroku_commands.go new file mode 100644 index 0000000..09e5eaf --- /dev/null +++ b/heroku_commands.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "os" + "time" + + "github.com/ParsePlatform/parse-cli/herokucmd" + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/ParsePlatform/parse-cli/webhooks" + "github.com/spf13/cobra" +) + +func herokuRootCmd(e *parsecli.Env) ([]string, *cobra.Command) { + c := &cobra.Command{ + Use: "parse", + Long: fmt.Sprintf( + `Parse Command Line Tool + +Tools to help you set up server code on Heroku +Version %v +Copyright %d Parse, Inc. +https://www.parse.com`, + parsecli.Version, + time.Now().Year(), + ), + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + c.AddCommand(NewAddCmd(e)) + c.AddCommand(NewConfigureCmd(e)) + c.AddCommand(NewDefaultCmd(e)) + c.AddCommand(herokucmd.NewDownloadCmd(e)) + c.AddCommand(herokucmd.NewDeployCmd(e)) + c.AddCommand(webhooks.NewFunctionHooksCmd(e)) + c.AddCommand(NewListCmd(e)) + c.AddCommand(herokucmd.NewLogsCmd(e)) + c.AddCommand(NewNewCmd(e)) + c.AddCommand(herokucmd.NewReleasesCmd(e)) + c.AddCommand(herokucmd.NewRollbackCmd(e)) + c.AddCommand(webhooks.NewTriggerHooksCmd(e)) + c.AddCommand(NewUpdateCmd(e)) + c.AddCommand(NewVersionCmd(e)) + + if len(os.Args) <= 1 { + return nil, c + } + commands := []string{"help"} + for _, command := range c.Commands() { + commands = append(commands, command.Name()) + } + + args := make([]string, len(os.Args)-1) + copy(args, os.Args[1:]) + + if message := parsecli.MakeCorrections(commands, args); message != "" { + fmt.Fprintln(e.Out, message) + } + c.SetArgs(args) + + return args, c +} diff --git a/herokucmd/add.go b/herokucmd/add.go new file mode 100644 index 0000000..3876e91 --- /dev/null +++ b/herokucmd/add.go @@ -0,0 +1,215 @@ +package herokucmd + +import ( + "fmt" + "math/rand" + "strconv" + "strings" + "text/tabwriter" + "time" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/stackerr" + "github.com/skratchdot/open-golang/open" +) + +const linkHerokuURL = "https://parse.com/account/edit" + +func selectHerokuApp(apps nameIDs, e *parsecli.Env) (*nameID, error) { + fmt.Fprintf(e.Out, "Please select from the following Heroku apps: (Enter a number between 1 and %d)\n", len(apps)) + for i, app := range apps { + w := new(tabwriter.Writer) + w.Init(e.Out, 0, 8, 0, '\t', 0) + fmt.Fprintf(w, "%d: %s\t\t(%s)\n", i+1, app.name, app.id) + if err := w.Flush(); err != nil { + return nil, stackerr.Wrap(err) + } + } + fmt.Fprintf(e.Out, "Selection: ") + var selection string + fmt.Fscanf(e.In, "%s\n", &selection) + + n, err := strconv.Atoi(selection) + if err != nil { + return nil, err + } + + lapps := len(apps) + if n <= 0 || n > lapps { + return nil, stackerr.Newf("Invalid selection: can only be in range 1..%d", lapps) + } + return &apps[n-1], nil +} + +func getRandomAppName(app *parsecli.App) string { + rand.Seed(time.Now().Unix()) + construct := fmt.Sprintf( + "%s-%s-%d", + strings.ToLower(app.Name), + strings.ToLower(app.ApplicationID[:6]), + rand.Intn(10000), + ) + l := len(construct) + if l > 25 { + l = 25 + } + if construct[l-1] == '-' { // ensure no dash at the end + l -= -1 + } + return construct[:l] +} + +func GetLinkedHerokuAppConfig(app *parsecli.App, e *parsecli.Env) (bool, *parsecli.HerokuAppConfig, error) { + h := &herokuLink{parseAppID: app.ApplicationID} + apps, err := h.getAppLinks(e) + if err != nil { + return false, nil, err + } + if len(apps) == 0 { + randomName := getRandomAppName(app) + appName := randomName + for i := 0; i < 3; i++ { + if appName == randomName { + fmt.Fprintf(e.Out, + `Let's create a new Heroku app in which server code will be run. +The Heroku app will be named: %q +Note that this can be changed later using Heroku API or Dashboard. + +`, + randomName, + ) + } else { + appName = h.getHerokuAppName(e) + } + + id, err := h.createNewLink(e, appName) + if err == nil { + return true, &parsecli.HerokuAppConfig{ + ParseAppID: app.ApplicationID, + HerokuAppID: id, + }, nil + } + if i == 2 { + return false, nil, err + } + + switch { + case stackerr.HasUnderlying(err, stackerr.MatcherFunc(herokuAppNameTaken)): + fmt.Fprintf(e.Err, "App name %s already taken.\nPlease try again...\n\n", appName) + appName = "" + + case stackerr.HasUnderlying(err, stackerr.MatcherFunc(herokuAccountNotLinked)): + fmt.Fprintf(e.Err, `Looks like you have not yet linked your Heroku Account to your Parse account. +Trying to open %q in the browser. +Please click "Link Heroku" button at the bottom. +`, + linkHerokuURL, + ) + appName = randomName + err := open.Run(linkHerokuURL) + if err != nil { + fmt.Fprintf(e.Err, + `Sorry, we couldn’t open the browser for you. Go to + %q +and click the "Link Heroku" button to link your Heroku account to Parse. +`, + linkHerokuURL, + ) + } + + fmt.Fprintf(e.Out, "Press ENTER when you are done linking your Heroku account to Parse: ") + var discard string + fmt.Fscanf(e.In, "%s\n", &discard) + + default: + return false, nil, err + } + } + } + + if len(apps) == 1 { + return false, &parsecli.HerokuAppConfig{ + ParseAppID: app.ApplicationID, + HerokuAppID: apps[0].id, + }, nil + } + + // NOTE: this part of code will not be used for now + for r := 0; r < 3; r++ { + selected, err := selectHerokuApp(apps, e) + if err != nil { + fmt.Fprintf(e.Err, "error: %s.\nPlease try again...\n", parsecli.ErrorString(e, err)) + continue + } + if selected.id != "" { + return false, &parsecli.HerokuAppConfig{ + ParseAppID: app.ApplicationID, + HerokuAppID: selected.id, + }, nil + } + id, err := h.createNewLink(e, selected.name) + if err != nil { + return false, nil, err + } + return false, &parsecli.HerokuAppConfig{ + ParseAppID: app.ApplicationID, + HerokuAppID: id, + }, nil + } + return false, nil, stackerr.New("failed to selected an heroku app in 3 attempts") +} + +func AddSelectedHerokuApp( + appName string, + appConfig *parsecli.HerokuAppConfig, + args []string, + makeDefault, verbose bool, + e *parsecli.Env, +) error { + config, err := parsecli.ConfigFromDir(e.Root) + if err != nil { + return err + } + herokuConfig, ok := config.(*parsecli.HerokuConfig) + if !ok { + return stackerr.New("Invalid Heroku config.") + } + + // add app to config + if _, ok := herokuConfig.Applications[appName]; ok { + return stackerr.Newf("App %s has already been added", appName) + } + + herokuConfig.Applications[appName] = appConfig + + if len(args) > 0 && args[0] != "" { + alias := args[0] + aliasConfig, ok := herokuConfig.Applications[alias] + if !ok { + herokuConfig.Applications[alias] = &parsecli.HerokuAppConfig{Link: appName} + } + if ok && aliasConfig.GetLink() != "" { + fmt.Fprintf(e.Out, "Overwriting alias: %q to point to %q\n", alias, appName) + herokuConfig.Applications[alias] = &parsecli.HerokuAppConfig{Link: appName} + } + } + + if makeDefault { + if _, ok := herokuConfig.Applications[parsecli.DefaultKey]; ok { + return stackerr.New(`Default key already set. To override default, use command "parse default"`) + } + herokuConfig.Applications[parsecli.DefaultKey] = &parsecli.HerokuAppConfig{Link: appName} + } + + if err := parsecli.StoreConfig(e, herokuConfig); err != nil { + return err + } + if verbose { + fmt.Fprintf(e.Out, "Written config for %q\n", appName) + if makeDefault { + fmt.Fprintf(e.Out, "Set %q as default\n", appName) + } + } + + return nil +} diff --git a/herokucmd/deploy.go b/herokucmd/deploy.go new file mode 100644 index 0000000..ff98a05 --- /dev/null +++ b/herokucmd/deploy.go @@ -0,0 +1,142 @@ +package herokucmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/ParsePlatform/parse-cli/webhooks" + "github.com/bgentry/heroku-go" + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type deployCmd struct { + Force bool + Description string +} + +func fetchHerokuAppInfo(e *parsecli.Env, appConfig *parsecli.HerokuAppConfig) (*heroku.App, error) { + herokuAppInfo, err := e.HerokuAPIClient.AppInfo(appConfig.HerokuAppID) + if err != nil { + return nil, stackerr.Newf( + "Unable to fetch app info for: %q from heroku.", + appConfig.HerokuAppID, + ) + } + return herokuAppInfo, nil +} + +func setHerokuConfigs( + e *parsecli.Env, + appConfig *parsecli.HerokuAppConfig, + options map[string]*string, +) error { + varsSet, err := e.HerokuAPIClient.ConfigVarUpdate( + appConfig.HerokuAppID, + options, + ) + if err != nil { + stackerr.New("Unable to set config vars to heroku.") + } + fmt.Fprintln(e.Out, "Successfully set the following vars to heroku:") + for key, val := range varsSet { + if key != "HOOKS_URL" { + val = parsecli.Last4(val) + } + fmt.Fprintln(e.Out, key, val) + } + return nil +} + +func (d *deployCmd) deploy(e *parsecli.Env, ctx *parsecli.Context) error { + appConfig, ok := ctx.AppConfig.(*parsecli.HerokuAppConfig) + if !ok { + return stackerr.New("Invalid config type.") + } + + authToken, err := ctx.AppConfig.GetApplicationAuth(e) + if err != nil { + return err + } + appInfo, err := fetchHerokuAppInfo(e, appConfig) + if err != nil { + return err + } + + // set appropriate heroku config vars + appKeys, err := parsecli.FetchAppKeys(e, appConfig.ParseAppID) + if err != nil { + return err + } + fmt.Fprintln(e.Out, "This Node.js webhooks project will be deployed to Heroku.") + err = setHerokuConfigs(e, + appConfig, + map[string]*string{ + "HOOKS_URL": &appInfo.WebURL, + "PARSE_APP_ID": &appConfig.ParseAppID, + "PARSE_MASTER_KEY": &appKeys.MasterKey, + "PARSE_WEBHOOK_KEY": &appKeys.WebhookKey, + }, + ) + if err != nil { + return err + } + + // push to heroku git + g := gitInfo{ + repo: e.Root, + authToken: authToken, + remote: fmt.Sprintf("git.heroku.com/%s.git", appInfo.Name), + description: d.Description, + } + err = g.whichGit() + if err != nil { + return err + } + err = g.isGitRepo(e) + if err != nil { + err = g.init(e) + if err != nil { + return err + } + } + dirty, err := g.isDirty(e) + if err != nil { + return err + } + + if dirty { + err := g.commit(e) + if err != nil { + return err + } + } + if err := g.push(e, d.Force); err != nil { + return err + } + + webhooksConfig := filepath.Join(e.Root, "webhooks.json") + if _, err := os.Lstat(webhooksConfig); err == nil { + h := &webhooks.Hooks{BaseURL: appInfo.WebURL} + return h.HooksCmd(e, ctx, []string{webhooksConfig}) + } + + return nil +} + +func NewDeployCmd(e *parsecli.Env) *cobra.Command { + var d deployCmd + cmd := &cobra.Command{ + Use: "deploy [app]", + Short: "Deploys server code to Heroku", + Long: "Deploys server code to Heroku.", + Run: parsecli.RunWithClient(e, d.deploy), + } + cmd.Flags().BoolVarP(&d.Force, "force", "f", d.Force, + "Forcefully deploy current contents even if they differ hugely from existing deploy.") + cmd.Flags().StringVarP(&d.Description, "description", "d", d.Description, + "Description is used as git commit message, if repo is dirty, before pushing to Heroku remote.") + return cmd +} diff --git a/herokucmd/download.go b/herokucmd/download.go new file mode 100644 index 0000000..bc97e83 --- /dev/null +++ b/herokucmd/download.go @@ -0,0 +1,41 @@ +package herokucmd + +import ( + "fmt" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type downloadCmd struct{} + +func (h *downloadCmd) run(e *parsecli.Env, ctx *parsecli.Context) error { + appConfig, ok := ctx.AppConfig.(*parsecli.HerokuAppConfig) + if !ok { + return stackerr.New("expected heroku project config type") + } + + authToken, err := appConfig.GetApplicationAuth(e) + if err != nil { + return err + } + + herokuAppName, err := parsecli.FetchHerokuAppName(appConfig.HerokuAppID, e) + if err != nil { + return err + } + gitURL := fmt.Sprintf("https://:%s@git.heroku.com/%s.git", authToken, herokuAppName) + return (&gitInfo{}).pull(e, gitURL) +} + +func NewDownloadCmd(e *parsecli.Env) *cobra.Command { + d := &downloadCmd{} + cmd := &cobra.Command{ + Use: "download", + Short: "Downloads the latest deployed server code from Heroku servers", + Long: "Downloads the latest deployed server code from Heroku servers.", + Run: parsecli.RunWithClient(e, d.run), + } + return cmd +} diff --git a/herokucmd/git.go b/herokucmd/git.go new file mode 100644 index 0000000..4141823 --- /dev/null +++ b/herokucmd/git.go @@ -0,0 +1,175 @@ +package herokucmd + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/stackerr" +) + +type gitInfo struct { + repo string + authToken string + remote string + description string +} + +func (g *gitInfo) whichGit() error { + _, err := exec.LookPath("git") + if err != nil { + return stackerr.New(`Unable to locate "git". +Please install "git" and ensure that you are able to run "git help" from the command prompt.`, + ) + } + return nil +} + +func (g *gitInfo) isGitRepo(e *parsecli.Env) error { + _, err := os.Lstat(filepath.Join(e.Root, ".git")) + if os.IsNotExist(err) { + return stackerr.Newf("%s is not a git repository. Please run 'git init'", e.Root) + } + return nil +} + +func (g *gitInfo) checkAbort(e *parsecli.Env) error { + err := g.whichGit() + if err != nil { + return err + } + return g.isGitRepo(e) +} + +func (g *gitInfo) runCmd(cmd *exec.Cmd, msg string) error { + err := cmd.Run() + if err == nil { + return nil + } + if _, ok := err.(*exec.ExitError); ok { + if msg != "" { + return stackerr.New(msg) + } + return stackerr.Newf("error executing: %q", strings.Join(cmd.Args, " ")) + } + return stackerr.Wrap(err) +} + +func (g *gitInfo) getDescription() string { + if g.description != "" { + return g.description + } + return fmt.Sprintf("committed by parse-cli at: %s", time.Now()) +} + +func (g *gitInfo) isDirty(e *parsecli.Env) (bool, error) { + if err := g.checkAbort(e); err != nil { + return false, err + } + + cmd := exec.Command("git", "status", "--porcelain") + var cmdOut, cmdErr bytes.Buffer + cmd.Stdout = &cmdOut + cmd.Stderr = &cmdErr + err := g.runCmd(cmd, fmt.Sprintf("Unable to determine the status of git repo: %s", g.repo)) + if err != nil { + fmt.Fprintln(e.Err, cmdErr.String()) + return false, err + } + + if cmdOut.Len() == 0 { + return false, nil + } + + fmt.Fprintf(e.Out, "Status of git repo: %q:\n%s", g.repo, cmdOut.String()) + return true, nil +} + +func (g *gitInfo) init(e *parsecli.Env) error { + err := g.whichGit() + if err != nil { + return err + } + + cmd := exec.Command("git", "init") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return g.runCmd(cmd, "") +} + +func (g *gitInfo) commit(e *parsecli.Env) error { + if err := g.checkAbort(e); err != nil { + return err + } + + cmd := exec.Command("git", "add", "-A", ".") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) + if err != nil { + return err + } + + cmd = exec.Command("git", "commit", "-a", "-m", g.getDescription()) + return g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) +} + +func (g *gitInfo) push(e *parsecli.Env, force bool) error { + if err := g.checkAbort(e); err != nil { + return err + } + command := []string{"push"} + if force { + command = append(command, "-f") + } + command = append(command, + fmt.Sprintf("https://:%s@%s", g.authToken, g.remote), + "master", + ) + cmd := exec.Command("git", command...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return g.runCmd( + cmd, + fmt.Sprintf("Unable to push to git remote: %s", g.remote), + ) +} + +func (g *gitInfo) isEmptyRepository(path string) (bool, error) { + var files bytes.Buffer + cmd := exec.Command("git", "-C", path, "ls-files") + cmd.Stdout = &files + cmd.Stderr = os.Stderr + if err := g.runCmd(cmd, ""); err != nil { + return false, err + } + return files.Len() == 0, nil +} + +func (g *gitInfo) pull(e *parsecli.Env, gitURL string) error { + if err := g.checkAbort(e); err != nil { + return err + } + pullCmd := exec.Command("git", "pull", gitURL) + pullCmd.Stdout = os.Stdout + pullCmd.Stdin = os.Stdin + return g.runCmd(pullCmd, "") +} + +func (g *gitInfo) clone(gitURL, path string) error { + _, err := exec.LookPath("git") + if err != nil { + return stackerr.New(`Unable to locate "git". +Please install "git" and ensure that you are able to run "git help" from the command prompt.`, + ) + } + + cmd := exec.Command("git", "clone", gitURL, path) + cmd.Stderr = os.Stderr + return g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) +} diff --git a/herokucmd/link.go b/herokucmd/link.go new file mode 100644 index 0000000..6b561b8 --- /dev/null +++ b/herokucmd/link.go @@ -0,0 +1,225 @@ +package herokucmd + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "regexp" + "sort" + "sync" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/errgroup" + "github.com/facebookgo/jsonpipe" + "github.com/facebookgo/stackerr" +) + +const maxRequests = 5 + +type herokuLink struct { + parseAppID string +} + +var ( + herokuNameTakenRegex = regexp.MustCompile("Name is already taken") + herokuAccountNotLinkedRegex = regexp.MustCompile("Parse account is not linked to Heroku") +) + +func herokuAppNameTaken(err error) bool { + return herokuNameTakenRegex.MatchString(err.Error()) +} + +func herokuAccountNotLinked(err error) bool { + return herokuAccountNotLinkedRegex.MatchString(err.Error()) +} + +func (h *herokuLink) getHerokuAppName(e *parsecli.Env) string { + fmt.Fprint(e.Out, + `Let's create a new Heroku app in which server code will be run. +You can either provide a name for the Heroku app or +just hit ENTER to use a randomly generated name. +Note that this can be changed later using Heroku API or Dashboard. + +Heroku App Name: `) + var name string + fmt.Fscanf(e.In, "%s\n", &name) + return name +} + +func (h *herokuLink) createNewLink(e *parsecli.Env, herokuAppName string) (string, error) { + var l parsecli.Login + _, err := l.AuthUserWithToken(e, true) + if err != nil { + return "", stackerr.Wrap(err) + } + + req, err := http.NewRequest( + "POST", + "herokuLink", + ioutil.NopCloser( + jsonpipe.Encode( + map[string]string{"herokuAppName": herokuAppName}, + ), + ), + ) + if err != nil { + return "", stackerr.Wrap(err) + } + req.Header = make(http.Header) + req.Header.Set("X-Parse-Application-Id", h.parseAppID) + req.Header.Set("X-Parse-Account-Key", l.Credentials.Token) + + resp, err := e.ParseAPIClient.RoundTrip(req) + if err != nil { + if herokuAppNameTaken(err) { + fmt.Fprintln(e.Err, ` +Please provide a unique name that might not already be taken on Heroku. +`) + } + return "", stackerr.Wrap(err) + } + result := &struct { + Name string `json:"name"` + ID string `json:"id"` + }{} + if err := json.NewDecoder(resp.Body).Decode(result); err != nil { + return "", stackerr.Wrap(err) + } + if result.Name != herokuAppName || result.ID == "" { + return "", stackerr.New("could not create heroku app link") + } + return result.ID, nil +} + +type nameID struct { + name string + id string +} + +type nameIDs []nameID + +func (n nameIDs) Len() int { + return len(n) +} + +func (n nameIDs) Swap(i, j int) { + n[i], n[j] = n[j], n[i] +} + +func (n nameIDs) Less(i, j int) bool { + if n[i].name > n[j].name { + return false + } + return n[i].name < n[j].name || n[i].id < n[j].id +} + +func (h *herokuLink) herokuAppNames(ids []string, e *parsecli.Env) (nameIDs, []string, error) { + var wg errgroup.Group + wg.Add(len(ids)) + maxParallel := make(chan struct{}, maxRequests) + + var ( + ret nameIDs + deletedLinks []string + retMutex sync.Mutex + deletedLinksMutex sync.Mutex + ) + + getAppName := func(id string) { + defer func() { + wg.Done() + <-maxParallel + }() + appName, err := parsecli.FetchHerokuAppName(id, e) + if err != nil { + if stackerr.HasUnderlying(err, stackerr.MatcherFunc(parsecli.HerokuAppNotFound)) { + deletedLinksMutex.Lock() + defer deletedLinksMutex.Unlock() + deletedLinks = append(deletedLinks, id) + return + } + wg.Error(err) // ignore error if corresponding heroku app was deleted + return + } + + retMutex.Lock() + defer retMutex.Unlock() + ret = append(ret, nameID{id: id, name: appName}) + } + + for _, id := range ids { + go getAppName(id) + } + err := wg.Wait() + sort.Sort(ret) + return ret, deletedLinks, stackerr.Wrap(err) +} + +func (h *herokuLink) getAppLinks(e *parsecli.Env) (nameIDs, error) { + var l parsecli.Login + _, err := l.AuthUserWithToken(e, true) + if err != nil { + return nil, err + } + + req, err := http.NewRequest( + "GET", + "herokuLink", + nil, + ) + if err != nil { + return nil, stackerr.Wrap(err) + } + req.Header = make(http.Header) + req.Header.Set("X-Parse-Application-Id", h.parseAppID) + req.Header.Set("X-Parse-Account-Key", l.Credentials.Token) + + resp, err := e.ParseAPIClient.RoundTrip(req) + if err != nil { + return nil, stackerr.Wrap(err) + } + type herokuAppID struct { + ID string `json:"herokuAppId,omitempty"` + } + result := &struct { + Apps []*herokuAppID `json:"herokuApps"` + }{} + if err := json.NewDecoder(resp.Body).Decode(result); err != nil { + return nil, stackerr.Wrap(err) + } + var ret []string + for _, herokuApp := range result.Apps { + if herokuApp.ID != "" { + ret = append(ret, herokuApp.ID) + } + } + names, deletedIDs, err := h.herokuAppNames(ret, e) + h.removeStaleLinks(e, l.Credentials.Token, deletedIDs) + return names, err +} + +// removeStaleLinks tries to best effort delete stale links +// where corresponding heroku app was deleted +func (h *herokuLink) removeStaleLinks(e *parsecli.Env, token string, removeLinks []string) { + for _, removeLink := range removeLinks { + req, err := http.NewRequest( + "DELETE", + "herokuLink", + jsonpipe.Encode( + map[string]string{"herokuAppId": removeLink}, + ), + ) + if err != nil { + continue + } + req.Header = make(http.Header) + req.Header.Set("X-Parse-Application-Id", h.parseAppID) + req.Header.Set("X-Parse-Account-Key", token) + + _, err = e.ParseAPIClient.RoundTrip(req) + if err != nil { + continue + } + } +} diff --git a/herokucmd/logs.go b/herokucmd/logs.go new file mode 100644 index 0000000..efb71b3 --- /dev/null +++ b/herokucmd/logs.go @@ -0,0 +1,59 @@ +package herokucmd + +import ( + "io" + "net/http" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/bgentry/heroku-go" + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type logsCmd struct { + tail bool + num int +} + +func (h *logsCmd) run(e *parsecli.Env, ctx *parsecli.Context) error { + hkConfig, ok := ctx.AppConfig.(*parsecli.HerokuAppConfig) + if !ok { + return stackerr.New("Unexpected config format") + } + + opts := &heroku.LogSessionCreateOpts{} + if h.num == 0 { + h.num = 50 + } + //source := "app" + //opts.Source = &source + opts.Lines = &h.num + opts.Tail = &h.tail + + session, err := e.HerokuAPIClient.LogSessionCreate( + hkConfig.HerokuAppID, + opts, + ) + if err != nil { + return stackerr.Wrap(err) + } + resp, err := http.Get(session.LogplexURL) + if err != nil { + return stackerr.Wrap(err) + } + _, err = io.Copy(e.Out, resp.Body) + return stackerr.Wrap(err) +} + +func NewLogsCmd(e *parsecli.Env) *cobra.Command { + h := &logsCmd{num: 50} + cmd := &cobra.Command{ + Use: "logs", + Short: "Fetch logs from heroku", + Long: "Fetch logs from heroku.", + Run: parsecli.RunWithClient(e, h.run), + } + cmd.Flags().BoolVarP(&h.tail, "follow", "f", h.tail, "Tail logs from server.") + cmd.Flags().IntVarP(&h.num, "num", "n", h.num, "Number of log lines to fetch.") + return cmd +} diff --git a/herokucmd/new.go b/herokucmd/new.go new file mode 100644 index 0000000..e1e87cf --- /dev/null +++ b/herokucmd/new.go @@ -0,0 +1,154 @@ +package herokucmd + +import ( + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/stackerr" +) + +const ( + nodeSampleURL = "https://github.com/pavanka/parse-webhooks/releases/download/2.0.0/node-sample.zip" + nodeSampleBase = "node-sample" +) + +func PromptCreateWebhooks(e *parsecli.Env) (string, error) { + selections := map[int]string{ + 1: "Heroku (https://www.heroku.com)", + 2: "Parse (https://parse.com/docs/cloudcode/guide)", + } + + msg := "Sorry! CLI supports only Parse Cloud Code and Heroku." + + projectType := 0 + for i := 0; i < 3; i++ { + fmt.Fprintf( + e.Out, + `Which of these providers would you like use for running your server code: +%d) %s +%d) %s +Type 1 or 2 to make a selection: `, + 1, selections[1], + 2, selections[2], + ) + fmt.Fscanf(e.In, "%d\n", &projectType) + fmt.Fprintln(e.Out) + switch projectType { + case 1: + return "heroku", nil + case 2: + return "parse", nil + } + fmt.Fprintln(e.Err, msg) + } + return "", stackerr.New(msg) +} + +func writeHerokuConfigs(e *parsecli.Env) error { + err := parsecli.CreateConfigWithContent( + filepath.Join(e.Root, parsecli.ParseProject), + fmt.Sprintf( + `{"project_type": %d}`, + parsecli.HerokuFormat, + ), + ) + if err != nil { + return err + } + return parsecli.CreateConfigWithContent( + filepath.Join(e.Root, parsecli.ParseLocal), + "{}", + ) +} + +func downloadNodeSample(e *parsecli.Env) (string, error) { + tmpZip, err := ioutil.TempFile("", "parse-node-sample") + if err != nil { + return "", stackerr.Wrap(err) + } + + resp, err := http.Get(nodeSampleURL) + if err != nil { + return "", stackerr.Wrap(err) + } + + _, err = io.Copy(tmpZip, resp.Body) + if err != nil { + return "", stackerr.Wrap(err) + } + return tmpZip.Name(), nil +} + +func setupNodeSample(e *parsecli.Env, dumpTemplate bool) error { + if !dumpTemplate { + err := os.MkdirAll(e.Root, 0755) + if err != nil { + return stackerr.Wrap(err) + } + return writeHerokuConfigs(e) + } + + sampleCode, err := downloadNodeSample(e) + if err != nil { + return err + } + defer os.RemoveAll(sampleCode) + + err = unzip(sampleCode, nodeSampleBase, e.Root) + if err != nil { + return err + } + return writeHerokuConfigs(e) +} + +func CloneNodeCode(e *parsecli.Env, isNew, onlyConfig bool, appConfig parsecli.AppConfig) (bool, error) { + cloneTemplate := false + if !isNew && !onlyConfig { + authToken, err := appConfig.GetApplicationAuth(e) + if err != nil { + return false, err + } + herokuAppConfig, ok := appConfig.(*parsecli.HerokuAppConfig) + if !ok { + return false, stackerr.New("invalid heroku app config") + } + + var gitURL string + g := &gitInfo{} + + herokuAppName, err := parsecli.FetchHerokuAppName(herokuAppConfig.HerokuAppID, e) + if err != nil { + return false, err + } + gitURL = fmt.Sprintf("https://:%s@git.heroku.com/%s.git", authToken, herokuAppName) + err = g.clone(gitURL, e.Root) + if err != nil { + fmt.Fprintf(e.Err, `Failed to fetch the latest deployed code from Heroku. +Please try "git clone %s %s". +Currently cloning the template project. +`, + gitURL, + e.Root, + ) + cloneTemplate = true + } else { + isEmpty, err := g.isEmptyRepository(e.Root) + if err != nil { + return false, err + } + if isEmpty { + if err := os.RemoveAll(e.Root); err != nil { + return false, stackerr.Wrap(err) + } + cloneTemplate = true + } + } + } + cloneTemplate = (isNew || cloneTemplate) && !onlyConfig + return cloneTemplate, setupNodeSample(e, cloneTemplate) +} diff --git a/herokucmd/releases.go b/herokucmd/releases.go new file mode 100644 index 0000000..95ed549 --- /dev/null +++ b/herokucmd/releases.go @@ -0,0 +1,55 @@ +package herokucmd + +import ( + "fmt" + "text/tabwriter" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/bgentry/heroku-go" + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type releasesCmd struct{} + +func (r *releasesCmd) printHerokuReleases(releases []heroku.Release, e *parsecli.Env) { + w := new(tabwriter.Writer) + w.Init(e.Out, 20, 4, 2, ' ', 0) + fmt.Fprintln(w, "Version\tUpdate At\tDescription") + for _, release := range releases { + fmt.Fprintf( + w, + "%d\t%s\tDeployed by: %q. %s\n\n", + release.Version, + release.UpdatedAt, + release.User.Email, + release.Description, + ) + } + w.Flush() +} + +func (r *releasesCmd) run(e *parsecli.Env, c *parsecli.Context) error { + appConfig, ok := c.AppConfig.(*parsecli.HerokuAppConfig) + if !ok { + return stackerr.New("Invalid app config.") + } + lr := &heroku.ListRange{Field: "version", Max: 10, Descending: true} + releases, err := e.HerokuAPIClient.ReleaseList(appConfig.HerokuAppID, lr) + if err != nil { + return stackerr.Wrap(err) + } + r.printHerokuReleases(releases, e) + return nil +} + +func NewReleasesCmd(e *parsecli.Env) *cobra.Command { + r := &releasesCmd{} + cmd := &cobra.Command{ + Use: "releases [app]", + Short: "Gets the Heroku releases for a Parse app", + Long: "Prints the last 10 releases made on Heroku", + Run: parsecli.RunWithClient(e, r.run), + } + return cmd +} diff --git a/herokucmd/rollback.go b/herokucmd/rollback.go new file mode 100644 index 0000000..bedc964 --- /dev/null +++ b/herokucmd/rollback.go @@ -0,0 +1,62 @@ +package herokucmd + +import ( + "fmt" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/bgentry/heroku-go" + "github.com/facebookgo/stackerr" + "github.com/spf13/cobra" +) + +type rollbackCmd struct { + ReleaseName string +} + +func (r *rollbackCmd) getLastSecondVersion(e *parsecli.Env, name string) (int, error) { + releases, err := e.HerokuAPIClient.ReleaseList(name, &heroku.ListRange{Field: "version", Max: 2, Descending: true}) + if err != nil { + return 0, stackerr.Wrap(err) + } + return releases[1].Version, nil +} + +func (r *rollbackCmd) run(e *parsecli.Env, c *parsecli.Context) error { + appConfig, ok := c.AppConfig.(*parsecli.HerokuAppConfig) + if !ok { + return stackerr.New("Invalid Heroku app config") + } + releaseName := r.ReleaseName + if releaseName == "" { + lastSecond, err := r.getLastSecondVersion(e, appConfig.HerokuAppID) + if err != nil { + return err + } + releaseName = fmt.Sprintf("%d", lastSecond) + } + release, err := e.HerokuAPIClient.ReleaseRollback(appConfig.HerokuAppID, releaseName) + if err != nil { + return stackerr.Wrap(err) + } + fmt.Fprintf( + e.Out, + `%s +Current version: %d +`, + release.Description, + release.Version, + ) + return nil +} + +func NewRollbackCmd(e *parsecli.Env) *cobra.Command { + r := rollbackCmd{} + cmd := &cobra.Command{ + Use: "rollback [app]", + Short: "Rolls back the version for given app at Heroku", + Long: "Rolls back the version for given app at Heroku.", + Run: parsecli.RunWithClient(e, r.run), + } + cmd.Flags().StringVarP(&r.ReleaseName, "release", "r", r.ReleaseName, "The release to rollback to") + return cmd +} diff --git a/herokucmd/unzip.go b/herokucmd/unzip.go new file mode 100644 index 0000000..d9207ea --- /dev/null +++ b/herokucmd/unzip.go @@ -0,0 +1,50 @@ +package herokucmd + +import ( + "archive/zip" + "io" + "os" + "path/filepath" + + "github.com/facebookgo/stackerr" +) + +func unzip(zfile string, base string, target string) error { + r, err := zip.OpenReader(zfile) + if err != nil { + return stackerr.Wrap(err) + } + defer r.Close() + + for _, f := range r.Reader.File { + z, err := f.Open() + if err != nil { + return stackerr.Wrap(err) + } + defer z.Close() + + name, err := filepath.Rel(base, f.Name) + if err != nil { + return stackerr.Wrap(err) + } + path := filepath.Join(target, name) + if f.FileInfo().IsDir() { + err := os.MkdirAll(path, f.Mode()) + if err != nil { + return stackerr.Wrap(err) + } + } else { + w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, f.Mode()) + if err != nil { + return stackerr.Wrap(err) + } + defer w.Close() + + _, err = io.Copy(w, z) + if err != nil { + return stackerr.Wrap(err) + } + } + } + return nil +} diff --git a/list.go b/list.go index b6b9d06..cd9e803 100644 --- a/list.go +++ b/list.go @@ -33,7 +33,7 @@ func (l *listCmd) run(e *parsecli.Env, args []string) error { l.printListOfApps(e) var apps parsecli.Apps - if err := apps.Login.AuthUser(e); err != nil { + if err := apps.Login.AuthUser(e, false); err != nil { return err } var appName string diff --git a/main.go b/main.go index ea34e5a..a487c2b 100644 --- a/main.go +++ b/main.go @@ -59,7 +59,7 @@ func main() { e.Root = parsecli.GetLegacyProjectRoot(&e, cur) } } - if e.Type != parsecli.LegacyParseFormat && e.Type != parsecli.ParseFormat { + if e.Type != parsecli.LegacyParseFormat && e.Type != parsecli.ParseFormat && e.Type != parsecli.HerokuFormat { fmt.Fprintf(e.Err, "Unknown project type %d.\n", e.Type) os.Exit(1) } @@ -74,6 +74,14 @@ func main() { os.Exit(1) } e.ParseAPIClient = apiClient + if e.Type == parsecli.HerokuFormat { + apiClient, err := parsecli.NewHerokuAPIClient(&e) + if err != nil { + fmt.Fprintln(e.Err, err) + os.Exit(1) + } + e.HerokuAPIClient = apiClient + } var ( rootCmd *cobra.Command @@ -82,6 +90,8 @@ func main() { switch e.Type { case parsecli.LegacyParseFormat, parsecli.ParseFormat: command, rootCmd = parseRootCmd(&e) + case parsecli.HerokuFormat: + command, rootCmd = herokuRootCmd(&e) } if len(command) == 0 || command[0] != "update" { diff --git a/new.go b/new.go index 534341e..376ed6d 100644 --- a/new.go +++ b/new.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/ParsePlatform/parse-cli/herokucmd" "github.com/ParsePlatform/parse-cli/parsecli" "github.com/ParsePlatform/parse-cli/parsecmd" "github.com/facebookgo/parse" @@ -23,31 +24,49 @@ type newCmd struct { codeLocation string // location of Cloud Code project } -func (n *newCmd) curlCommand(app *parsecli.App) string { +func (n *newCmd) curlCommand(e *parsecli.Env, app *parsecli.App) string { + args := "{}" + if e.Type == parsecli.HerokuFormat { + args = `{"a": "Adventurous ", "b": "Parser"}` + } + return fmt.Sprintf( `curl -X POST \ -H "X-Parse-Application-Id: %s" \ -H "X-Parse-REST-API-Key: %s" \ -H "Content-Type: application/json" \ - -d '{}' \ + -d '%s' \ https://api.parse.com/1/functions/hello `, app.ApplicationID, app.RestKey, + args, ) } func (n *newCmd) cloudCodeHelpMessage(e *parsecli.Env, app *parsecli.App) string { + code := "Cloud Code" + if e.Type == parsecli.HerokuFormat { + code = "server code" + } return fmt.Sprintf( - `Your Cloud Code has been created at %s. -Next, you might want to deploy this code with "parse deploy". -This includes a "Hello world" cloud function, so once you deploy -you can test that it works, with: + `Your %s has been created at %s. + +This includes a "Hello world" cloud function, so once you deploy, +you can test that it works, with the printed curl command. +Next, you might want to deploy this code with: + + cd %s + parse deploy + +Once deployed you can test that it works by running: %s `, + code, + e.Root, e.Root, - n.curlCommand(app), + n.curlCommand(e, app), ) } @@ -198,19 +217,39 @@ Please refrain from creating a Parse project inside another Parse project. var decision string if isNew { fmt.Fprint(e.Out, ` -You can either set up a blank Cloud Code project or create a template project. -Please type [y] if you wish to set up a template Cloud Code project or [n] for blank project (y/n): `) +You can either set up a blank project or create a sample Cloud Code project. +Please type "(b)lank" if you wish to setup a blank project, otherwise press ENTER: `) } else { fmt.Fprint(e.Out, ` -You can either set up a blank Cloud Code project or download the current deployed Cloud Code. -Please type [y] if you wish to download the current Cloud Code or [n] for blank project (y/n): `) +You can either set up a blank project or download the current deployed Cloud Code. +Please type "(b)lank" if you wish to setup a blank project, otherwise press ENTER: `) } fmt.Fscanf(e.In, "%s\n", &decision) - if strings.TrimSpace(decision) == "n" { + decision = strings.ToLower(strings.TrimSpace(decision)) + if decision != "" && decision == "b" || decision == "blank" { n.configOnly = true } } return parsecmd.CloneSampleCloudCode(e, isNew, n.configOnly, appConfig) + case parsecli.HerokuFormat: + if !n.configOnly { + var decision string + if isNew { + fmt.Fprint(e.Out, ` +You can either set up a blank project or create a sample Node.js project. +Please type "(b)lank" if you wish to setup a blank project, otherwise press ENTER: `) + } else { + fmt.Fprint(e.Out, ` +You can either set up a blank project or download the server code currently deployed to Heroku. +Please type "(b)lank" if you wish to setup a blank project, otherwise press ENTER: `) + } + fmt.Fscanf(e.In, "%s\n", &decision) + decision = strings.ToLower(strings.TrimSpace(decision)) + if decision != "" && decision == "b" || decision == "blank" { + n.configOnly = true + } + } + return herokucmd.CloneNodeCode(e, isNew, n.configOnly, appConfig) } return false, stackerr.Newf("Unknown project type: %d", e.Type) } @@ -247,7 +286,7 @@ func (n *newCmd) run(e *parsecli.Env) error { apps := &parsecli.Apps{} addCmd := &addCmd{MakeDefault: true, apps: apps} - if err := apps.Login.AuthUser(e); err != nil { + if err := apps.Login.AuthUser(e, false); err != nil { return err } var ( @@ -290,8 +329,26 @@ func (n *newCmd) run(e *parsecli.Env) error { } } - e.Type = parsecli.ParseFormat - appConfig := parsecmd.GetParseAppConfig(app) + projectType, err := herokucmd.PromptCreateWebhooks(e) + if err != nil { + return err + } + + var appConfig parsecli.AppConfig + switch projectType { + case "heroku": + e.Type = parsecli.HerokuFormat + var newHerokuApp bool + newHerokuApp, appConfig, err = herokucmd.GetLinkedHerokuAppConfig(app, e) + if err != nil { + return err + } + isNew = isNew || newHerokuApp + + case "parse": + e.Type = parsecli.ParseFormat + appConfig = parsecmd.GetParseAppConfig(app) + } dumpTemplate, err := n.setupSample(e, app.Name, appConfig, isNew, nonInteractive) if err != nil { diff --git a/new_test.go b/new_test.go index 7b980b7..6f3f166 100644 --- a/new_test.go +++ b/new_test.go @@ -32,9 +32,9 @@ func TestCloudCodeHelpMessage(t *testing.T) { msg, fmt.Sprintf( `Your Cloud Code has been created at %s. -Next, you might want to deploy this code with "parse deploy". -This includes a "Hello world" cloud function, so once you deploy -you can test that it works, with:`, + +This includes a "Hello world" cloud function, so once you deploy, +you can test that it works, with`, h.Env.Root, ), ) @@ -137,8 +137,11 @@ func TestNewCmdContent(t *testing.T) { func TestCurlCommand(t *testing.T) { t.Parallel() + h := parsecli.NewHarness(t) + defer h.Stop() + n := &newCmd{} - command := n.curlCommand(&parsecli.App{ApplicationID: "AppID", RestKey: "RestKey"}) + command := n.curlCommand(h.Env, &parsecli.App{ApplicationID: "AppID", RestKey: "RestKey"}) ensure.DeepEqual(t, command, `curl -X POST \ diff --git a/parsecli/apps.go b/parsecli/apps.go index e93f1bb..730f6e6 100644 --- a/parsecli/apps.go +++ b/parsecli/apps.go @@ -90,9 +90,9 @@ func getAuthHeaders(credentials *Credentials, headers http.Header) http.Header { return headers } -func fetchAppKeys(e *Env, appID string) (*App, error) { +func FetchAppKeys(e *Env, appID string) (*App, error) { l := &Login{} - err := l.AuthUser(e) + err := l.AuthUser(e, false) if err != nil { return nil, err } diff --git a/parsecli/apps_test.go b/parsecli/apps_test.go index 364b6d9..fc41b01 100644 --- a/parsecli/apps_test.go +++ b/parsecli/apps_test.go @@ -26,7 +26,7 @@ func TestFetchAppKeys(t *testing.T) { defer h.Stop() h.Env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) - app, err := fetchAppKeys(h.Env, "an-app") + app, err := FetchAppKeys(h.Env, "an-app") ensure.Nil(t, err) ensure.DeepEqual(t, app, newTestApp("an-app")) } diff --git a/parsecli/client.go b/parsecli/client.go index f0bd35e..a719ea7 100644 --- a/parsecli/client.go +++ b/parsecli/client.go @@ -4,6 +4,7 @@ import ( "net/http" "net/url" + "github.com/bgentry/heroku-go" "github.com/facebookgo/parse" "github.com/facebookgo/stackerr" ) @@ -25,6 +26,10 @@ func NewParseAPIClient(e *Env) (*ParseAPIClient, error) { }, nil } +func NewHerokuAPIClient(e *Env) (*heroku.Client, error) { + return &heroku.Client{}, nil +} + func (c *ParseAPIClient) appendCommonHeaders(header http.Header) http.Header { if header == nil { header = make(http.Header) diff --git a/parsecli/config.go b/parsecli/config.go index c1ce8bd..ac518d6 100644 --- a/parsecli/config.go +++ b/parsecli/config.go @@ -13,6 +13,7 @@ import ( const ( LegacyParseFormat = iota ParseFormat + HerokuFormat ParseLocal = ".parse.local" ParseProject = ".parse.project" @@ -111,6 +112,17 @@ func ConfigFromDir(dir string) (Config, error) { } c.ProjectConfig = p return c, nil + + case HerokuFormat: + c, err := readHerokuConfigFile(configFile) + if err != nil { + return nil, canonicalize(err) + } + if c.Applications == nil { + c.Applications = make(map[string]*HerokuAppConfig) + } + c.ProjectConfig = p + return c, nil } return nil, stackerr.Newf("Unknown project type: %d.", p.Type) @@ -138,7 +150,7 @@ func StoreConfig(e *Env, c Config) error { lconf, filepath.Join(e.Root, LegacyConfigFile), ) - case ParseFormat: + case ParseFormat, HerokuFormat: return writeConfigFile(c, filepath.Join(e.Root, ParseLocal)) } return stackerr.Newf("Unknown project type: %d.", projectType) @@ -181,7 +193,7 @@ func StoreProjectConfig(e *Env, c Config) error { filepath.Join(e.Root, LegacyConfigFile), ) - case ParseFormat: + case ParseFormat, HerokuFormat: return writeProjectConfig(p, filepath.Join(e.Root, ParseProject)) } @@ -236,6 +248,13 @@ func SetDefault(e *Env, newDefault, defaultApp string, c Config) error { return stackerr.New("Invalid Cloud Code config.") } return setParseDefault(e, newDefault, defaultApp, p) + + case HerokuFormat: + h, ok := c.(*HerokuConfig) + if !ok { + return stackerr.New("Invalid Cloud Code config.") + } + return SetHerokuDefault(e, newDefault, defaultApp, h) } return stackerr.Newf("Unknown project type: %d.", projectType) diff --git a/parsecli/context.go b/parsecli/context.go index 03067c4..a337d82 100644 --- a/parsecli/context.go +++ b/parsecli/context.go @@ -1,6 +1,11 @@ package parsecli -import "github.com/facebookgo/parse" +import ( + "fmt" + "net/http" + + "github.com/facebookgo/parse" +) type Context struct { Config Config @@ -30,6 +35,17 @@ func newContext(e *Env, appName string) (*Context, error) { }, ) + if e.HerokuAPIClient != nil { + authToken, err := app.GetApplicationAuth(e) + if err != nil { + return nil, err + } + headers := make(http.Header) + headers.Add("Authorization", fmt.Sprintf("Bearer %s", authToken)) + e.HerokuAPIClient.AdditionalHeaders = headers + e.HerokuAPIClient.UserAgent = UserAgent + } + return &Context{ AppName: appName, AppConfig: app, diff --git a/parsecli/heroku_config.go b/parsecli/heroku_config.go new file mode 100644 index 0000000..850189c --- /dev/null +++ b/parsecli/heroku_config.go @@ -0,0 +1,258 @@ +package parsecli + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "regexp" + "sort" + + "github.com/facebookgo/jsonpipe" + "github.com/facebookgo/stackerr" +) + +type HerokuAppConfig struct { + ParseAppID string `json:"parseAppId,omitempty"` + MasterKey string `json:"masterKey,omitempty"` + HerokuAppID string `json:"herokuAppId,omitempty"` + HerokuAccessToken string `json:"herokuAccessToken,omitempty"` + Link string `json:"link,omitempty"` + + masterKey string + herokuAccessToken string +} + +func (c *HerokuAppConfig) WithHiddenMasterKey(masterKey string) *HerokuAppConfig { + c.masterKey = masterKey + return c +} + +func (c *HerokuAppConfig) WithHiddenAccessToken(accessToken string) *HerokuAppConfig { + c.herokuAccessToken = accessToken + return c +} + +func (c *HerokuAppConfig) GetApplicationID() string { + return c.ParseAppID +} + +func (c *HerokuAppConfig) GetMasterKey(e *Env) (string, error) { + if c.MasterKey != "" { + return c.MasterKey, nil + } + if c.masterKey != "" { + return c.masterKey, nil + } + app, err := FetchAppKeys(e, c.GetApplicationID()) + if err != nil { + return "", err + } + c.masterKey = app.MasterKey + return c.masterKey, nil +} + +func (c *HerokuAppConfig) GetApplicationAuth(e *Env) (string, error) { + if c.herokuAccessToken != "" { + return c.herokuAccessToken, nil + } + + var l Login + _, err := l.AuthUserWithToken(e, true) + if err != nil { + return "", err + } + req, err := http.NewRequest( + "POST", + "herokuToken", + ioutil.NopCloser( + jsonpipe.Encode( + map[string]string{ + "id": c.HerokuAppID, + }, + ), + ), + ) + if err != nil { + return "", stackerr.Wrap(err) + } + req.Header = make(http.Header) + req.Header.Set("X-Parse-Application-Id", c.ParseAppID) + req.Header.Set("X-Parse-Account-Key", l.Credentials.Token) + + resp, err := e.ParseAPIClient.RoundTrip(req) + if err != nil { + return "", stackerr.Wrap(err) + } + result := &struct { + Token string `json:"token"` + }{} + if err := json.NewDecoder(resp.Body).Decode(result); err != nil { + return "", stackerr.Wrap(err) + } + c.herokuAccessToken = result.Token + return result.Token, nil +} + +func (c *HerokuAppConfig) GetLink() string { + return c.Link +} + +type HerokuConfig struct { + Applications map[string]*HerokuAppConfig `json:"applications,omityempty"` + ProjectConfig *ProjectConfig `json:"-"` +} + +func (c *HerokuConfig) AddAlias(name, link string) error { + if _, found := c.Applications[name]; found { + return stackerr.Newf("App %q has already been added.", name) + } + if _, found := c.Applications[link]; !found { + return stackerr.Newf("App %q wasn't found.", link) + } + c.Applications[name] = &HerokuAppConfig{Link: link} + return nil +} + +func (c *HerokuConfig) SetDefaultApp(name string) error { + delete(c.Applications, DefaultKey) + return c.AddAlias(DefaultKey, name) +} + +func (c *HerokuConfig) App(name string) (AppConfig, error) { + ac, found := c.Applications[name] + if !found { + if name == DefaultKey { + return nil, stackerr.Newf("No default app configured.") + } + return nil, stackerr.Newf("App %q wasn't found.", name) + } + if ac.Link != "" { + return c.App(ac.Link) + } + return ac, nil +} + +func (c *HerokuConfig) GetProjectConfig() *ProjectConfig { + return c.ProjectConfig +} + +func (c *HerokuConfig) GetDefaultApp() string { + var defaultApp string + if defaultKeyLink, ok := c.Applications[DefaultKey]; ok { + defaultApp = defaultKeyLink.Link + } + return defaultApp +} + +func (c *HerokuConfig) GetNumApps() int { + return len(c.Applications) +} + +var herokuAppNotFoundRegex = regexp.MustCompile("App not found") + +func HerokuAppNotFound(err error) bool { + return herokuAppNotFoundRegex.MatchString(err.Error()) +} + +func FetchHerokuAppName(id string, e *Env) (string, error) { + req, err := http.NewRequest( + "GET", + fmt.Sprintf("https://api.heroku.com/apps/%s", id), + nil, + ) + if err != nil { + return "", stackerr.Wrap(err) + } + resp, err := e.ParseAPIClient.RoundTrip(req) + if err != nil { + return "", stackerr.Wrap(err) + } + app := struct { + Name string `json:"name"` + }{} + if err := json.NewDecoder(resp.Body).Decode(&app); err != nil { + return "", stackerr.Wrap(err) + } + return app.Name, nil +} + +func (c *HerokuConfig) PrettyPrintApps(e *Env) { + apps := c.Applications + + defaultApp := c.GetDefaultApp() + + var appNames []string + for appName := range apps { + appNames = append(appNames, appName) + } + sort.Strings(appNames) + + if len(appNames) == 0 { + return + } + + fmt.Fprintln( + e.Out, + "The following apps are associated with cloud code in the current directory:", + ) + + for _, appName := range appNames { + if appName == DefaultKey { + continue + } + if defaultApp == appName { + fmt.Fprint(e.Out, "* ") + } else { + fmt.Fprint(e.Out, " ") + } + fmt.Fprintf(e.Out, "%s", appName) + + config, _ := apps[appName] + if config.GetLink() != "" { + fmt.Fprintf(e.Out, " -> %s", config.GetLink()) + } + herokuAppName, err := FetchHerokuAppName(config.HerokuAppID, e) + if err != nil { + if stackerr.HasUnderlying(err, stackerr.MatcherFunc(HerokuAppNotFound)) { + herokuAppName = "" + } else { + herokuAppName = config.HerokuAppID + } + } + fmt.Fprintf(e.Out, " (%q)\n", herokuAppName) + } +} + +func readHerokuConfigFile(path string) (*HerokuConfig, error) { + f, err := os.Open(path) + if err != nil { + return nil, stackerr.Wrap(err) + } + defer f.Close() + var c HerokuConfig + if err := json.NewDecoder(f).Decode(&c); err != nil { + return nil, stackerr.Newf("Config file %q is not valid JSON.", path) + } + return &c, nil +} + +func SetHerokuDefault(e *Env, newDefault, defaultApp string, herokuConfig *HerokuConfig) error { + apps := herokuConfig.Applications + if _, ok := apps[newDefault]; !ok { + herokuConfig.PrettyPrintApps(e) + return stackerr.Newf("Invalid application name \"%s\". Please select from the valid applications printed above.", newDefault) + } + + if defaultApp == "" { + apps[DefaultKey] = &HerokuAppConfig{Link: newDefault} + } else { + apps[DefaultKey].Link = newDefault + } + if err := StoreConfig(e, herokuConfig); err != nil { + return err + } + fmt.Fprintf(e.Out, "Default app set to %s.\n", newDefault) + return nil +} diff --git a/parsecli/login.go b/parsecli/login.go index 49b1c5d..1c8f991 100644 --- a/parsecli/login.go +++ b/parsecli/login.go @@ -42,12 +42,16 @@ to reset your password`) tokenErrMsgf = `Sorry, the account key: %q you provided is not valid. Please follow instructions at %q to generate a new one. ` - keyNotFound = regexp.MustCompile("Could not find account key") - parseNetrc = filepath.Join(".parse", "netrc") + keyNotConfigured = regexp.MustCompile("Account key not configured") + + parseNetrc = filepath.Join(".parse", "netrc") ) -func accessKeyNotFound(err error) bool { - return keyNotFound.MatchString(err.Error()) +func accountKeyNotConfigured(err error) bool { + if err == nil { + return false + } + return keyNotConfigured.MatchString(err.Error()) } func (l *Login) populateCreds(e *Env) error { @@ -231,12 +235,34 @@ func (l *Login) AuthToken(e *Env, token string) (string, error) { return res.Email, nil } -func (l *Login) authUserWithToken(e *Env) (string, error) { +func (l *Login) AuthUserWithToken(e *Env, strict bool) (string, error) { _, tokenCredentials, err := l.GetTokenCredentials(e, e.ParserEmail) if err != nil { - if stackerr.HasUnderlying(err, stackerr.MatcherFunc(accessKeyNotFound)) { - fmt.Fprintln(e.Err, ErrorString(e, err)) + // user never created an account key: educate them + if stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) { + if strict { + fmt.Fprintln( + e.Out, + `To proceed further, you must configure an account key. +`, + ) + } else { + fmt.Fprintln( + e.Out, + `We've changed the way the CLI works. +To save time logging in, you should create an account key. +`, + ) + + } + + fmt.Fprintln( + e.Out, + `Type "parse configure accountkey" to create a new account key. +Read more at: https://parse.com/docs/js/guide#command-line-account-keys`) + return "", stackerr.New("Account key not configured") } + return "", err } @@ -250,29 +276,26 @@ func (l *Login) authUserWithToken(e *Env) (string, error) { return email, nil } -func (l *Login) AuthUser(e *Env) error { - _, err := l.authUserWithToken(e) +func (l *Login) AuthUser(e *Env, strict bool) error { + _, err := l.AuthUserWithToken(e, strict) if err == nil { return nil } + if strict { + return err + } - // user never created an account key: educate them - if stackerr.HasUnderlying(err, stackerr.MatcherFunc(os.IsNotExist)) { + if !stackerr.HasUnderlying(err, stackerr.MatcherFunc(accountKeyNotConfigured)) { fmt.Fprintln( e.Out, - `We've changed the way the CLI works. -To save time logging in, you should create an account key. -`) + `Type "parse configure accountkey" to create a new account key. +Read more at: https://parse.com/docs/js/guide#command-line-account-keys + +Please login to Parse using your email and password.`, + ) } apps := &Apps{} - fmt.Fprintln( - e.Out, - `Type "parse configure accountkey" to create a new account key. -Read more at: https://parse.com/docs/cloudcode/guide#command-line-account-keys - -Please login to Parse using your email and password.`, - ) for i := 0; i < numRetries; i++ { err := l.populateCreds(e) if err != nil { diff --git a/parsecli/login_test.go b/parsecli/login_test.go index 2d44d25..b865f99 100644 --- a/parsecli/login_test.go +++ b/parsecli/login_test.go @@ -48,7 +48,7 @@ func TestGetTokenCredentials(t *testing.T) { ) h.Env.Server = "http://api.parse.com" _, credentials, err = l.GetTokenCredentials(h.Env, "") - ensure.Err(t, err, keyNotFound) + ensure.Err(t, err, regexp.MustCompile("Could not find account key")) l = &Login{} h.Env.Server = "http://api.example.com/1/" @@ -71,7 +71,7 @@ func TestGetTokenCredentials(t *testing.T) { ) h.Env.Server = "http://api.parse.com" _, credentials, err = l.GetTokenCredentials(h.Env, "email") - ensure.Err(t, err, keyNotFound) + ensure.Err(t, err, regexp.MustCompile("Could not find account key")) l = &Login{} h.Env.Server = "http://api.example.com/1/" @@ -120,7 +120,7 @@ func TestAuthUserWithToken(t *testing.T) { password token `, ) - _, err := l.authUserWithToken(h.Env) + _, err := l.AuthUserWithToken(h.Env, false) ensure.Nil(t, err) h.Env.ParserEmail = "email2" @@ -131,7 +131,7 @@ func TestAuthUserWithToken(t *testing.T) { password token `, ) - _, err = l.authUserWithToken(h.Env) + _, err = l.AuthUserWithToken(h.Env, false) ensure.Err(t, err, regexp.MustCompile(`does not belong to "email2"`)) h.Env.ParserEmail = "" @@ -142,7 +142,7 @@ func TestAuthUserWithToken(t *testing.T) { password token2 `, ) - _, err = l.authUserWithToken(h.Env) + _, err = l.AuthUserWithToken(h.Env, false) ensure.Err(t, err, regexp.MustCompile("provided is not valid")) } diff --git a/parsecli/main.go b/parsecli/main.go index e277596..7e1eca6 100644 --- a/parsecli/main.go +++ b/parsecli/main.go @@ -14,6 +14,7 @@ import ( "strings" "testing" + "github.com/bgentry/heroku-go" "github.com/facebookgo/clock" "github.com/facebookgo/ensure" "github.com/facebookgo/parse" @@ -21,7 +22,7 @@ import ( ) const ( - Version = "2.2.9" + Version = "3.0.0" CloudDir = "cloud" HostingDir = "public" DefaultBaseURL = "https://api.parse.com/1/" @@ -30,17 +31,18 @@ const ( var UserAgent = fmt.Sprintf("parse-cli-%s-%s", runtime.GOOS, Version) type Env struct { - Root string // project root - Server string // parse api server - Type int // project type - ParserEmail string // email associated with developer parse account - ErrorStack bool - Out io.Writer - Err io.Writer - In io.Reader - Exit func(int) - Clock clock.Clock - ParseAPIClient *ParseAPIClient + Root string // project root + Server string // parse api server + Type int // project type + ParserEmail string // email associated with developer parse account + ErrorStack bool + Out io.Writer + Err io.Writer + In io.Reader + Exit func(int) + Clock clock.Clock + ParseAPIClient *ParseAPIClient + HerokuAPIClient *heroku.Client } type Harness struct { diff --git a/parsecli/parse_config.go b/parsecli/parse_config.go index 27e272d..acac657 100644 --- a/parsecli/parse_config.go +++ b/parsecli/parse_config.go @@ -37,7 +37,7 @@ func (c *ParseAppConfig) GetMasterKey(e *Env) (string, error) { if c.masterKey != "" { return c.masterKey, nil } - app, err := fetchAppKeys(e, c.GetApplicationID()) + app, err := FetchAppKeys(e, c.GetApplicationID()) if err != nil { return "", err } From 9768aad7b883c00a3e13ad599071015e03e7016f Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 22 Oct 2015 10:36:21 -0700 Subject: [PATCH 080/106] update readme message --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2c9d022..e31a62a 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ Parse Command Line Tool ======= [![Build Status](https://travis-ci.org/ParsePlatform/parse-cli.svg?branch=master)](https://travis-ci.org/ParsePlatform/parse-cli) -The `Parse Command Line Tool` allows you to interact with your Cloud Code from the terminal. +The `Parse Command Line Tool` allows you to set up your Parse app's server-side code from the terminal. +You can deploy your server-side code to either Parse Cloud Code or Heroku Node.js. -For more information please visit [Parse](https://parse.com/docs/cloudcode/guide#command-line). +For more information please visit [Parse](https://parse.com/docs/cloudcode/guide). Overview -------- From da58adc65b7709403d8ec1feba6ede36e73b83bd Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 22 Oct 2015 10:38:39 -0700 Subject: [PATCH 081/106] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e31a62a..b69c9be 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Parse Command Line Tool The `Parse Command Line Tool` allows you to set up your Parse app's server-side code from the terminal. You can deploy your server-side code to either Parse Cloud Code or Heroku Node.js. -For more information please visit [Parse](https://parse.com/docs/cloudcode/guide). +To learn more, please visit our [docs](https://parse.com/docs/cloudcode/guide). Overview -------- From 759ab05b05b8cc68b5b6f8a3b51e885ec71cc99d Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 28 Oct 2015 20:04:00 +0000 Subject: [PATCH 082/106] [issue-41] heroku app name for app name with spaces when parse app name has spaces, the heroku app name we create should remove such spaces before creating the corresponding heroku app name --- herokucmd/add.go | 8 +++++++- herokucmd/add_test.go | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 herokucmd/add_test.go diff --git a/herokucmd/add.go b/herokucmd/add.go index 3876e91..0195d7c 100644 --- a/herokucmd/add.go +++ b/herokucmd/add.go @@ -43,9 +43,15 @@ func selectHerokuApp(apps nameIDs, e *parsecli.Env) (*nameID, error) { func getRandomAppName(app *parsecli.App) string { rand.Seed(time.Now().Unix()) + + namePrefix := strings.Join(strings.Fields(strings.ToLower(app.Name)), "") + if namePrefix == "" { + namePrefix = "parsoku" + } + construct := fmt.Sprintf( "%s-%s-%d", - strings.ToLower(app.Name), + namePrefix, strings.ToLower(app.ApplicationID[:6]), rand.Intn(10000), ) diff --git a/herokucmd/add_test.go b/herokucmd/add_test.go new file mode 100644 index 0000000..2382ce7 --- /dev/null +++ b/herokucmd/add_test.go @@ -0,0 +1,14 @@ +package herokucmd + +import ( + "testing" + + "github.com/ParsePlatform/parse-cli/parsecli" + "github.com/facebookgo/ensure" +) + +func TestGetRandomAppName(t *testing.T) { + app := &parsecli.App{Name: "test app", ApplicationID: "123456789"} + name := getRandomAppName(app) + ensure.StringContains(t, name, "testapp") +} From eb09eb1779ee50f0a62954adbb3d30e351052fa9 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 30 Oct 2015 04:11:47 +0000 Subject: [PATCH 083/106] record mode of operation of command line - command line can run in 2 modes, parse/heroku - record which mode it was run in for debugging --- herokucmd/new.go | 14 ++++++++++++++ main.go | 8 ++++++-- main_test.go | 4 ++-- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/herokucmd/new.go b/herokucmd/new.go index e1e87cf..439a076 100644 --- a/herokucmd/new.go +++ b/herokucmd/new.go @@ -5,6 +5,7 @@ import ( "io" "io/ioutil" "net/http" + "net/url" "os" "path/filepath" @@ -17,6 +18,17 @@ const ( nodeSampleBase = "node-sample" ) +func recordDecision(e *parsecli.Env, decision string) { + v := make(url.Values) + v.Set("version", parsecli.Version) + v.Set("decision", decision) + req := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, + } + e.ParseAPIClient.Do(req, nil, nil) +} + func PromptCreateWebhooks(e *parsecli.Env) (string, error) { selections := map[int]string{ 1: "Heroku (https://www.heroku.com)", @@ -40,8 +52,10 @@ Type 1 or 2 to make a selection: `, fmt.Fprintln(e.Out) switch projectType { case 1: + recordDecision(e, "heroku") return "heroku", nil case 2: + recordDecision(e, "parse") return "parse", nil } fmt.Fprintln(e.Err, msg) diff --git a/main.go b/main.go index a487c2b..b08d8a2 100644 --- a/main.go +++ b/main.go @@ -87,15 +87,18 @@ func main() { rootCmd *cobra.Command command []string ) + var mode string switch e.Type { case parsecli.LegacyParseFormat, parsecli.ParseFormat: + mode = "parse" command, rootCmd = parseRootCmd(&e) case parsecli.HerokuFormat: + mode = "heroku" command, rootCmd = herokuRootCmd(&e) } if len(command) == 0 || command[0] != "update" { - message, err := checkIfSupported(&e, parsecli.Version, command...) + message, err := checkIfSupported(&e, parsecli.Version, mode, command...) if err != nil { fmt.Fprintln(e.Err, err) os.Exit(1) @@ -111,9 +114,10 @@ func main() { } } -func checkIfSupported(e *parsecli.Env, version string, args ...string) (string, error) { +func checkIfSupported(e *parsecli.Env, version string, mode string, args ...string) (string, error) { v := make(url.Values) v.Set("version", version) + v.Set("mode", mode) v.Set("other", strings.Join(args, " ")) req := &http.Request{ Method: "GET", diff --git a/main_test.go b/main_test.go index d5d831d..127470a 100644 --- a/main_test.go +++ b/main_test.go @@ -37,7 +37,7 @@ func TestIsSupportedWarning(t *testing.T) { }, nil }) h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} - message, err := checkIfSupported(h.Env, "2.0.2") + message, err := checkIfSupported(h.Env, "2.0.2", "parse") ensure.Nil(t, err) ensure.DeepEqual(t, message, "please update") } @@ -60,7 +60,7 @@ func TestIsSupportedError(t *testing.T) { }, nil }) h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} - _, err := checkIfSupported(h.Env, "2.0.2") + _, err := checkIfSupported(h.Env, "2.0.2", "parse") ensure.Err(t, err, regexp.MustCompile("not supported")) } From 1e5f0a997b800f571eda45dc1c289335ef9b3d91 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 30 Oct 2015 04:32:45 +0000 Subject: [PATCH 084/106] bump version to 3.0.1 - bump version - also set time to check for supported version to 1s --- main.go | 2 +- parsecli/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index b08d8a2..78e1a3a 100644 --- a/main.go +++ b/main.go @@ -141,7 +141,7 @@ func checkIfSupported(e *parsecli.Env, version string, mode string, args ...stri select { case res := <-timeout: return res.warning, stackerr.Wrap(res.err) - case <-time.After(time.Duration(500) * time.Millisecond): + case <-time.After(time.Second): return "", nil } return "", nil diff --git a/parsecli/main.go b/parsecli/main.go index 7e1eca6..914dfd7 100644 --- a/parsecli/main.go +++ b/parsecli/main.go @@ -22,7 +22,7 @@ import ( ) const ( - Version = "3.0.0" + Version = "3.0.1" CloudDir = "cloud" HostingDir = "public" DefaultBaseURL = "https://api.parse.com/1/" From 668e1eb0495680e0f60cf3f2eebdff2a9428f9be Mon Sep 17 00:00:00 2001 From: Joe Lutz Date: Sat, 7 Nov 2015 14:03:07 -0600 Subject: [PATCH 085/106] update link to account keys documentation --- parsecli/login.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/parsecli/login.go b/parsecli/login.go index 1c8f991..3f179f6 100644 --- a/parsecli/login.go +++ b/parsecli/login.go @@ -259,7 +259,7 @@ To save time logging in, you should create an account key. fmt.Fprintln( e.Out, `Type "parse configure accountkey" to create a new account key. -Read more at: https://parse.com/docs/js/guide#command-line-account-keys`) +Read more at: https://parse.com/docs/cloudcode/guide#command-line-account-keys`) return "", stackerr.New("Account key not configured") } @@ -289,7 +289,7 @@ func (l *Login) AuthUser(e *Env, strict bool) error { fmt.Fprintln( e.Out, `Type "parse configure accountkey" to create a new account key. -Read more at: https://parse.com/docs/js/guide#command-line-account-keys +Read more at: https://parse.com/docs/cloudcode/guide#command-line-account-keys Please login to Parse using your email and password.`, ) From 9a35e1274d4e78095b1cb09bd511a9d34a83a547 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 3 Dec 2015 18:06:27 +0000 Subject: [PATCH 086/106] add new line to selection prompt looks like %s can consume everything in unix systems but in windows new line is not put into %s, this causes a weird case where new line is passed onto the next scanf causing confusion while selecting an app during new flow for windows users. identified via a bug report --- parsecli/apps.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsecli/apps.go b/parsecli/apps.go index 730f6e6..0492261 100644 --- a/parsecli/apps.go +++ b/parsecli/apps.go @@ -148,7 +148,7 @@ func (a *Apps) SelectApp(apps []*App, msg string, e *Env) (*App, error) { fmt.Fprintf(e.Out, "%s%s", selectionString(appNames), msg) var selected string - fmt.Fscanf(e.In, "%s", &selected) + fmt.Fscanf(e.In, "%s\n", &selected) if pos, err = strconv.Atoi(strings.TrimSpace(selected)); err != nil { pos = -1 break From 19d03766daa1ad5a0f04703abac143afa62225e7 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 3 Dec 2015 18:34:59 +0000 Subject: [PATCH 087/106] abort new workflow when dir exists abort new workflow even when a directory with given name exists. currently we only abort if a file with the same name exists. reported via bug tool --- new.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/new.go b/new.go index 376ed6d..8b50a44 100644 --- a/new.go +++ b/new.go @@ -123,21 +123,22 @@ so it does not conflict with any other Cloud Code in the current directory. cloudCodeDirInfo, err := os.Stat(filepath.Join(e.Root, cloudCodeDir)) if err == nil { - if !cloudCodeDirInfo.IsDir() { - return "", stackerr.Newf(`Sorry, we are unable to create Cloud Code at %s. -In the current directory a file named: %s already exists. + fileType := "file" + if cloudCodeDirInfo.IsDir() { + fileType = "directory" + } + return "", stackerr.Newf(`Sorry, we are unable to create Cloud Code at %s. +In the current directory a %s named: %q already exists. Please run "parse new" again. %s Please choose a different name for your Cloud Code directory, so it does not conflict with any other Cloud Code in the current directory. `, - cloudCodeDir, - cloudCodeDir, - helpMsg, - ) - - } - return "", nil + cloudCodeDir, + fileType, + cloudCodeDir, + helpMsg, + ) } if !os.IsNotExist(err) { return "", stackerr.Wrap(err) From 9bb1ececc3a6909fddefd9bd5ef65818f51a3104 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 3 Dec 2015 18:47:24 +0000 Subject: [PATCH 088/106] [release] release 3.0.2 version of parse cli --- parsecli/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsecli/main.go b/parsecli/main.go index 914dfd7..fd69ea7 100644 --- a/parsecli/main.go +++ b/parsecli/main.go @@ -22,7 +22,7 @@ import ( ) const ( - Version = "3.0.1" + Version = "3.0.2" CloudDir = "cloud" HostingDir = "public" DefaultBaseURL = "https://api.parse.com/1/" From c25e4cc417445c8ef2398af1402de494ff45dad4 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 3 Dec 2015 18:54:50 +0000 Subject: [PATCH 089/106] fix test break due to existing dir fix test break due to existing dir error which is a change in behavior introduced in previous commit --- new_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/new_test.go b/new_test.go index 6f3f166..779831d 100644 --- a/new_test.go +++ b/new_test.go @@ -80,8 +80,7 @@ func TestGetCloudCodeDir(t *testing.T) { h.Out.Reset() h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) name, err = n.getCloudCodeDir(h.Env, "myapp", false) - ensure.Nil(t, err) - ensure.StringContains(t, h.Out.String(), "folder where we can download the latest") + ensure.Err(t, err, regexp.MustCompile("a directory named: \"myapp\" already exists")) } func TestNewCmdDirs(t *testing.T) { From 57e0bd7a4804c7ed6ab704c32829b8047e077f02 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Wed, 23 Dec 2015 05:22:19 +0000 Subject: [PATCH 090/106] [heroku-sample] update the template code for heroku update the sample code that is setup when heroku apps are created --- herokucmd/new.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/herokucmd/new.go b/herokucmd/new.go index 439a076..6c23860 100644 --- a/herokucmd/new.go +++ b/herokucmd/new.go @@ -14,7 +14,7 @@ import ( ) const ( - nodeSampleURL = "https://github.com/pavanka/parse-webhooks/releases/download/2.0.0/node-sample.zip" + nodeSampleURL = "https://github.com/pavanka/parse-webhooks/releases/download/3.0.0/node-sample.zip" nodeSampleBase = "node-sample" ) From bf914769ddea80fecc712c1c1568d7c12494ef39 Mon Sep 17 00:00:00 2001 From: Shannon Whitley Date: Mon, 18 Jan 2016 18:13:57 -0800 Subject: [PATCH 091/106] Command line quotes for Windows --- new.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/new.go b/new.go index 8b50a44..0a68d13 100644 --- a/new.go +++ b/new.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "strconv" "github.com/ParsePlatform/parse-cli/herokucmd" "github.com/ParsePlatform/parse-cli/parsecli" @@ -25,17 +26,17 @@ type newCmd struct { } func (n *newCmd) curlCommand(e *parsecli.Env, app *parsecli.App) string { - args := "{}" + args := "\"{}\"" if e.Type == parsecli.HerokuFormat { - args = `{"a": "Adventurous ", "b": "Parser"}` + args = strconv.Quote(`{"a": "Adventurous ", "b": "Parser"}`) } - + return fmt.Sprintf( `curl -X POST \ -H "X-Parse-Application-Id: %s" \ -H "X-Parse-REST-API-Key: %s" \ -H "Content-Type: application/json" \ - -d '%s' \ + -d %s \ https://api.parse.com/1/functions/hello `, app.ApplicationID, From 0016473719fa214dc7519e33e19e214c01a8a226 Mon Sep 17 00:00:00 2001 From: Shannon Whitley Date: Sat, 23 Jan 2016 14:49:48 -0800 Subject: [PATCH 092/106] Revert "Command line quotes for Windows" This reverts commit bf914769ddea80fecc712c1c1568d7c12494ef39. --- new.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/new.go b/new.go index 0a68d13..8b50a44 100644 --- a/new.go +++ b/new.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "strings" - "strconv" "github.com/ParsePlatform/parse-cli/herokucmd" "github.com/ParsePlatform/parse-cli/parsecli" @@ -26,17 +25,17 @@ type newCmd struct { } func (n *newCmd) curlCommand(e *parsecli.Env, app *parsecli.App) string { - args := "\"{}\"" + args := "{}" if e.Type == parsecli.HerokuFormat { - args = strconv.Quote(`{"a": "Adventurous ", "b": "Parser"}`) + args = `{"a": "Adventurous ", "b": "Parser"}` } - + return fmt.Sprintf( `curl -X POST \ -H "X-Parse-Application-Id: %s" \ -H "X-Parse-REST-API-Key: %s" \ -H "Content-Type: application/json" \ - -d %s \ + -d '%s' \ https://api.parse.com/1/functions/hello `, app.ApplicationID, From 0b1a439693de3d9daa761cb6bc2e0360b0bb6c52 Mon Sep 17 00:00:00 2001 From: Shannon Whitley Date: Sat, 23 Jan 2016 14:53:45 -0800 Subject: [PATCH 093/106] Command line quotes for Windows --- new.go | 5 +++-- new_test.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/new.go b/new.go index 8b50a44..7660a64 100644 --- a/new.go +++ b/new.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "strconv" "github.com/ParsePlatform/parse-cli/herokucmd" "github.com/ParsePlatform/parse-cli/parsecli" @@ -35,12 +36,12 @@ func (n *newCmd) curlCommand(e *parsecli.Env, app *parsecli.App) string { -H "X-Parse-Application-Id: %s" \ -H "X-Parse-REST-API-Key: %s" \ -H "Content-Type: application/json" \ - -d '%s' \ + -d %s \ https://api.parse.com/1/functions/hello `, app.ApplicationID, app.RestKey, - args, + strconv.Quote(args), ) } diff --git a/new_test.go b/new_test.go index 779831d..e29b173 100644 --- a/new_test.go +++ b/new_test.go @@ -147,7 +147,7 @@ func TestCurlCommand(t *testing.T) { -H "X-Parse-Application-Id: AppID" \ -H "X-Parse-REST-API-Key: RestKey" \ -H "Content-Type: application/json" \ - -d '{}' \ + -d "{}" \ https://api.parse.com/1/functions/hello `) } From f93d2d5f0b63d0d10dd3782f5f1e833b098430e8 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Mon, 25 Jan 2016 09:03:42 +0000 Subject: [PATCH 094/106] [build] fix build break due to incorrect dependency by cobra --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2dd9bf2..593da91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ go: script: - go test -v ./... before_deploy: + - go get github.com/inconshreveable/mousetrap # temporary fix for travis build breaks for windows due to cobra dependency on mousetrap - go get github.com/mitchellh/gox - gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" -build-toolchain - gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" -output="parse_{{.OS}}_{{.Arch}}" From f6841620a8e8dccab962351fbc986077a1443ec7 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 3 Mar 2016 16:23:40 -0800 Subject: [PATCH 095/106] update go version to 1.5.3 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 593da91..0d8df51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: go go: - - 1.4.2 + - 1.5.3 script: - go test -v ./... before_deploy: From 94f8e34dfc3d3b592e3ff2f2992c5d9a4953edf2 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 4 Mar 2016 00:33:14 +0000 Subject: [PATCH 096/106] use go 1.5.3 to cross compile instead of gox --- .travis.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0d8df51..a55008b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,12 +5,10 @@ script: - go test -v ./... before_deploy: - go get github.com/inconshreveable/mousetrap # temporary fix for travis build breaks for windows due to cobra dependency on mousetrap - - go get github.com/mitchellh/gox - - gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" -build-toolchain - - gox -osarch="darwin/amd64" -osarch="linux/arm" -osarch="linux/386" -osarch="windows/386" -output="parse_{{.OS}}_{{.Arch}}" - - mv parse_darwin_amd64 parse - - mv parse_windows_386.exe parse.exe - - mv parse_linux_386 parse_linux + - GOOS=darwin GOARCH=amd64 go build -v github.com/ParsePlatform/parse-cli -o parse + - GOOS=windows GOARCH=386 go build -v github.com/ParsePlatform/parse-cli -o parse.exe + - GOOS=linux GOARCH=386 go build -v github.com/ParsePlatform/parse-cli -o parse_linux + - GOOS=linux GOARCH=arm go build -v github.com/ParsePlatform/parse-cli -o parse_linux_arm deploy: provider: releases api_key: From 6a3d66d4f090e0b388cda66f3a6d8ffa15736263 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Thu, 3 Mar 2016 16:51:00 -0800 Subject: [PATCH 097/106] fix output path in .travis.ci --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a55008b..18f822e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,10 @@ script: - go test -v ./... before_deploy: - go get github.com/inconshreveable/mousetrap # temporary fix for travis build breaks for windows due to cobra dependency on mousetrap - - GOOS=darwin GOARCH=amd64 go build -v github.com/ParsePlatform/parse-cli -o parse - - GOOS=windows GOARCH=386 go build -v github.com/ParsePlatform/parse-cli -o parse.exe - - GOOS=linux GOARCH=386 go build -v github.com/ParsePlatform/parse-cli -o parse_linux - - GOOS=linux GOARCH=arm go build -v github.com/ParsePlatform/parse-cli -o parse_linux_arm + - GOOS=darwin GOARCH=amd64 go build -o parse -v github.com/ParsePlatform/parse-cli + - GOOS=windows GOARCH=386 go build -o parse.exe -v github.com/ParsePlatform/parse-cli + - GOOS=linux GOARCH=386 go build -o parse_linux -v github.com/ParsePlatform/parse-cli + - GOOS=linux GOARCH=arm go build -o parse_linux_arm -v github.com/ParsePlatform/parse-cli deploy: provider: releases api_key: From 0ce499daef7b61757cfb58075a015d48a9dfaf30 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Date: Fri, 4 Mar 2016 04:00:34 -0800 Subject: [PATCH 098/106] bump cli version to 3.0.5 --- parsecli/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parsecli/main.go b/parsecli/main.go index fd69ea7..14a7d18 100644 --- a/parsecli/main.go +++ b/parsecli/main.go @@ -22,7 +22,7 @@ import ( ) const ( - Version = "3.0.2" + Version = "3.0.5" CloudDir = "cloud" HostingDir = "public" DefaultBaseURL = "https://api.parse.com/1/" From a281a7a8ff3d586c6653a89832581048bb0bfb4b Mon Sep 17 00:00:00 2001 From: Kevin Lacker Date: Wed, 5 Apr 2017 13:21:22 -0700 Subject: [PATCH 099/106] transfer code --- LICENSE | 4 ++++ PATENTS | 4 ++++ README.md | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/LICENSE b/LICENSE index ab89c2b..1b757f1 100644 --- a/LICENSE +++ b/LICENSE @@ -28,3 +28,7 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. diff --git a/PATENTS b/PATENTS index 964cdb7..c0ac110 100644 --- a/PATENTS +++ b/PATENTS @@ -31,3 +31,7 @@ necessarily infringed by the Software standing alone. A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, or contributory infringement or inducement to infringe any patent, including a cross-claim or counterclaim. + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. diff --git a/README.md b/README.md index b69c9be..ed73308 100644 --- a/README.md +++ b/README.md @@ -48,3 +48,7 @@ The following commands are currently available in the `Parse Command Line Tool`: version Gets the Command Line Tools version help Help about any command ``` + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. From e0fba36cb5544f7ceb7bbc564612d1eac7c35e5c Mon Sep 17 00:00:00 2001 From: Tom Fox Date: Wed, 20 Mar 2019 21:52:12 +0000 Subject: [PATCH 100/106] Create CODE_OF_CONDUCT.md --- CODE_OF_CONDUCT.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8ca0e00 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at codeofconduct@parseplatform.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq From a32a9be05bad852daa5a30ae6385f02a15b09d14 Mon Sep 17 00:00:00 2001 From: Tom Fox Date: Fri, 29 Mar 2019 20:10:58 +0000 Subject: [PATCH 101/106] remove 404 links (#76) --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index ed73308..336293f 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,12 @@ Parse Command Line Tool The `Parse Command Line Tool` allows you to set up your Parse app's server-side code from the terminal. You can deploy your server-side code to either Parse Cloud Code or Heroku Node.js. -To learn more, please visit our [docs](https://parse.com/docs/cloudcode/guide). - Overview -------- Parse Command Line Tool can be used to perform various actions on your Parse app. It can be used to create new Parse apps, deploy Cloud Code to an app, view all releases for an app, etc. -You can install `Parse Command Line Tool` by following instructions at [parse-cli](https://parse.com/apps/quickstart#cloud_code/). - -Alternatively, you can just type the following command. +To install `Parse Command Line Tool`, you can just type the following command. NOTE: You should already have [Go](https://golang.org/doc/install) installed and GOPATH, GOROOT set to appropriate values. From 4d949b7d5a988a2a2b832fb7037288ada91d690f Mon Sep 17 00:00:00 2001 From: Tom Fox Date: Sat, 30 Mar 2019 19:39:04 +0000 Subject: [PATCH 102/106] Update README.md --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 336293f..1c48e0e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ Parse Command Line Tool ======= -[![Build Status](https://travis-ci.org/ParsePlatform/parse-cli.svg?branch=master)](https://travis-ci.org/ParsePlatform/parse-cli) +[![Build Status](https://travis-ci.org/parse-community/parse-cli.svg?branch=master)](https://travis-ci.org/parse-community/parse-cli) +[![Join The Conversation](https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg)](https://community.parseplatform.org/c/parse-server) +[![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)](#backers) +[![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)](#sponsors) +[![License][license-svg]][license-link] +![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social) The `Parse Command Line Tool` allows you to set up your Parse app's server-side code from the terminal. You can deploy your server-side code to either Parse Cloud Code or Heroku Node.js. @@ -48,3 +53,7 @@ The following commands are currently available in the `Parse Command Line Tool`: ----- As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. + +[license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg +[license-link]: https://github.com/parse-community/Parse-SDK-JS/blob/master/LICENSE + From a24633d9cab84171565e7eb64c78153eb3aee09a Mon Sep 17 00:00:00 2001 From: Tom Fox Date: Sun, 31 Mar 2019 11:26:50 +0100 Subject: [PATCH 103/106] fix license link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1c48e0e..6216de6 100644 --- a/README.md +++ b/README.md @@ -55,5 +55,5 @@ The following commands are currently available in the `Parse Command Line Tool`: As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. [license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg -[license-link]: https://github.com/parse-community/Parse-SDK-JS/blob/master/LICENSE +[license-link]: LICENSE From afe52fba6213b2780ab4dcb7803fcd38b140c332 Mon Sep 17 00:00:00 2001 From: Tom Fox Date: Mon, 1 Apr 2019 22:10:03 +0100 Subject: [PATCH 104/106] fix badge links --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6216de6..d0d290d 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ Parse Command Line Tool ======= [![Build Status](https://travis-ci.org/parse-community/parse-cli.svg?branch=master)](https://travis-ci.org/parse-community/parse-cli) [![Join The Conversation](https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg)](https://community.parseplatform.org/c/parse-server) -[![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)](#backers) -[![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)](#sponsors) +[![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)][open-collective-link] +[![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)][open-collective-link] [![License][license-svg]][license-link] -![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social) +[![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social)](https://twitter.com/intent/follow?screen_name=ParsePlatform) The `Parse Command Line Tool` allows you to set up your Parse app's server-side code from the terminal. You can deploy your server-side code to either Parse Cloud Code or Heroku Node.js. @@ -56,4 +56,4 @@ As of April 5, 2017, Parse, LLC has transferred this code to the parse-community [license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg [license-link]: LICENSE - +[open-collective-link]: https://opencollective.com/parse-server From 5832d123c53260db97f9d93e55c312d9054847e0 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Wed, 10 Apr 2019 06:54:37 -0400 Subject: [PATCH 105/106] Update README.md (#78) --- README.md | 59 +++---------------------------------------------------- 1 file changed, 3 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index d0d290d..25ba258 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,6 @@ -Parse Command Line Tool -======= -[![Build Status](https://travis-ci.org/parse-community/parse-cli.svg?branch=master)](https://travis-ci.org/parse-community/parse-cli) -[![Join The Conversation](https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg)](https://community.parseplatform.org/c/parse-server) -[![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)][open-collective-link] -[![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)][open-collective-link] -[![License][license-svg]][license-link] -[![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social)](https://twitter.com/intent/follow?screen_name=ParsePlatform) - -The `Parse Command Line Tool` allows you to set up your Parse app's server-side code from the terminal. -You can deploy your server-side code to either Parse Cloud Code or Heroku Node.js. - -Overview --------- -Parse Command Line Tool can be used to perform various actions on your Parse app. -It can be used to create new Parse apps, deploy Cloud Code to an app, view all releases for an app, etc. - -To install `Parse Command Line Tool`, you can just type the following command. - -NOTE: You should already have [Go](https://golang.org/doc/install) installed and GOPATH, GOROOT set to appropriate values. -```bash - go get -t github.com/ParsePlatform/parse-cli -``` -This installs a binary called `parse-cli` at `$GOPATH/bin`. -Further, you can find the code for Parse CLI at: `$GOPATH/src/github.com/ParsePlatform/parse-cli`. - -The following commands are currently available in the `Parse Command Line Tool`: -```bash - add Adds a new Parse app to config in current Cloud Code directory - configure Configure various Parse settings - default Sets or gets the default Parse app - deploy Deploys a Parse app - develop Monitors for changes to code and deploys, also tails parse logs - download Downloads the Cloud Code project - functions List Cloud Code functions and function webhooks - generate Generates a sample express app in the current project directory - jssdk Sets the Parse JavaScript SDK version to use in Cloud Code - list Lists properties of the given Parse app and Parse apps associated with given project - logs Prints out recent log messages - migrate Migrate project config format to the preferred format - new Adds Cloud Code to an existing Parse app, can also create a new Parse app - releases Gets the releases for a Parse app - rollback Rolls back the version for the given app - symbols Uploads symbol files - triggers List Cloud Code triggers and trigger webhooks - update Updates this tool to the latest version - version Gets the Command Line Tools version - help Help about any command -``` - ------ - -As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. +ARCHIVE - Parse Command Line Tool +======= -[license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg -[license-link]: LICENSE -[open-collective-link]: https://opencollective.com/parse-server +This project is retired. From eec3be4d6a766064ae17bc316b6125da2970a038 Mon Sep 17 00:00:00 2001 From: Tom Fox Date: Thu, 25 Apr 2019 20:48:49 +0100 Subject: [PATCH 106/106] restore readme / add warning --- README.md | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 25ba258..a09abe5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,61 @@ +> ## WARNING ⚠️ - this project has been retired due to lack of use & contribution, if you wish to continue to use it please fork or if you wish to maintain this project make yourself known ⚠️ - -ARCHIVE - Parse Command Line Tool +Parse Command Line Tool ======= +[![Build Status](https://travis-ci.org/parse-community/parse-cli.svg?branch=master)](https://travis-ci.org/parse-community/parse-cli) +[![Join The Conversation](https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg)](https://community.parseplatform.org/c/parse-server) +[![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)][open-collective-link] +[![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)][open-collective-link] +[![License][license-svg]][license-link] +[![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social)](https://twitter.com/intent/follow?screen_name=ParsePlatform) + +The `Parse Command Line Tool` allows you to set up your Parse app's server-side code from the terminal. +You can deploy your server-side code to either Parse Cloud Code or Heroku Node.js. + +Overview +-------- +Parse Command Line Tool can be used to perform various actions on your Parse app. +It can be used to create new Parse apps, deploy Cloud Code to an app, view all releases for an app, etc. + +To install `Parse Command Line Tool`, you can just type the following command. + +NOTE: You should already have [Go](https://golang.org/doc/install) installed and GOPATH, GOROOT set to appropriate values. + +```bash + go get -t github.com/ParsePlatform/parse-cli +``` + +This installs a binary called `parse-cli` at `$GOPATH/bin`. +Further, you can find the code for Parse CLI at: `$GOPATH/src/github.com/ParsePlatform/parse-cli`. + +The following commands are currently available in the `Parse Command Line Tool`: +```bash + add Adds a new Parse app to config in current Cloud Code directory + configure Configure various Parse settings + default Sets or gets the default Parse app + deploy Deploys a Parse app + develop Monitors for changes to code and deploys, also tails parse logs + download Downloads the Cloud Code project + functions List Cloud Code functions and function webhooks + generate Generates a sample express app in the current project directory + jssdk Sets the Parse JavaScript SDK version to use in Cloud Code + list Lists properties of the given Parse app and Parse apps associated with given project + logs Prints out recent log messages + migrate Migrate project config format to the preferred format + new Adds Cloud Code to an existing Parse app, can also create a new Parse app + releases Gets the releases for a Parse app + rollback Rolls back the version for the given app + symbols Uploads symbol files + triggers List Cloud Code triggers and trigger webhooks + update Updates this tool to the latest version + version Gets the Command Line Tools version + help Help about any command +``` + +----- + +As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. -This project is retired. +[license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg +[license-link]: LICENSE +[open-collective-link]: https://opencollective.com/parse-server