├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── PATENTS ├── README.md ├── add.go ├── add_test.go ├── configure.go ├── configure_test.go ├── default.go ├── default_test.go ├── heroku_commands.go ├── herokucmd ├── add.go ├── add_test.go ├── deploy.go ├── download.go ├── git.go ├── link.go ├── logs.go ├── new.go ├── releases.go ├── rollback.go └── unzip.go ├── list.go ├── list_test.go ├── main.go ├── main_test.go ├── migrate.go ├── migrate_test.go ├── new.go ├── new_test.go ├── parse_commands.go ├── parsecli ├── apps.go ├── apps_test.go ├── auto_correct.go ├── auto_correct_test.go ├── client.go ├── config.go ├── config_test.go ├── context.go ├── heroku_config.go ├── legacy_config.go ├── legacy_config_test.go ├── login.go ├── login_test.go ├── main.go ├── parse_config.go ├── runners.go ├── runners_test.go ├── utils.go └── utils_test.go ├── parsecmd ├── Resources │ ├── AndroidManifest.xml │ ├── Test.xcarchive │ │ ├── Info.plist │ │ └── dSYMs │ │ │ └── Test.app.dSYM │ │ │ └── Contents │ │ │ ├── Info.plist │ │ │ └── Resources │ │ │ └── DWARF │ │ │ └── Test │ ├── build_tools │ │ └── .empty │ └── mapping.txt ├── add.go ├── android_symbol_uploader.go ├── android_symbol_uploader_test.go ├── deploy.go ├── deploy_test.go ├── develop.go ├── develop_test.go ├── download.go ├── download_test.go ├── generate.go ├── generate_test.go ├── ios_symbol_uploader.go ├── ios_symbol_uploader_test.go ├── jssdk.go ├── jssdk_test.go ├── logs.go ├── logs_test.go ├── new.go ├── parse_symbol_uploader.go ├── parse_symbol_uploader_test.go ├── parseignore.go ├── parseignore_test.go ├── releases.go ├── releases_test.go ├── rollback.go ├── rollback_test.go ├── symbols.go ├── symbols_test.go ├── templates.go ├── utils.go └── utils_test.go ├── update.go ├── update_test.go ├── utils.go ├── version.go ├── version_test.go └── webhooks ├── functions.go ├── functions_test.go ├── hooks.go ├── hooks_test.go ├── triggers.go └── triggers_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.5.3 4 | script: 5 | - go test -v ./... 6 | before_deploy: 7 | - go get github.com/inconshreveable/mousetrap # temporary fix for travis build breaks for windows due to cobra dependency on mousetrap 8 | - GOOS=darwin GOARCH=amd64 go build -o parse -v github.com/ParsePlatform/parse-cli 9 | - GOOS=windows GOARCH=386 go build -o parse.exe -v github.com/ParsePlatform/parse-cli 10 | - GOOS=linux GOARCH=386 go build -o parse_linux -v github.com/ParsePlatform/parse-cli 11 | - GOOS=linux GOARCH=arm go build -o parse_linux_arm -v github.com/ParsePlatform/parse-cli 12 | deploy: 13 | provider: releases 14 | api_key: 15 | secure: ZnquaugjmlJ6geWvXCVjZeN2Z1NZyOaNWbY1hY8LAZZR+QqlRUR+6+QZ85vHYnqHAqKHDoh8BcZUR+2hK9UV2Lqsq0aTzvuxxVIKFiF1Gc3eC6zK3FUPgk9eOnxPZM1REGM7/i977HckpNVirskdHK1esdkKOYIfLQaHW75+xZYNJ/RKUceVrrT99IlOLWH+52vd/xGY3osV65FWkF8QS8LsZrqDQlrGyeht/P5YxepkyvMxGhBs7O3j5Npldmvdce5AQttY4MXQkMXkIrbQ2u4auBtw94hn9POd2msO0pq9JF76Is3FsH4B0/3AUrWkxMHV5XAwK1ziXniVKqQeIzaPadV92r8ihySYOQABCfQgQrHaH03jG+zE4CqqxbQvVUCCoszTZRg0AFAbaYeTKngN8kaaEEiJrKsmEASHfc8cql2TYH9Tr+ZvF/dxyYiBDU/i+BjJ4Y5jpoyXrUFeeLKRSSN4WGYYaC0r4aCBNcVDQFo9sd0n1lUtznc6wtg671Qgrvq/aog7I4qJKYBeJ/OGm771IboPxLob3P5hBdMZwOFPbjFsOmMhyBvYwxBJz3SJfVW/+pCCQdfqbfHSB0ZawdeRBQdm3w24hvq5wdpk5nGTdbdCbxb/9LsVd1WuU4xE8PiTpXxn1WqiYEcVjFd2NtYkBtFliAvnEKax+Ew= 16 | file: 17 | - parse 18 | - parse.exe 19 | - parse_linux 20 | - parse_linux_arm 21 | skip_cleanup: true 22 | on: 23 | repo: ParsePlatform/parse-cli 24 | tags: true 25 | condition: $TRAVIS_TAG =~ ^release_.* 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at codeofconduct@parseplatform.org. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For parse-cli software 4 | 5 | Copyright (c) 2015, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | ----- 33 | 34 | 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. 35 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the parse-cli software distributed by Facebook, Inc. 4 | 5 | Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software 6 | ("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable 7 | (subject to the termination provision below) license under any Necessary 8 | Claims, to make, have made, use, sell, offer to sell, import, and otherwise 9 | transfer the Software. For avoidance of doubt, no license is granted under 10 | Facebook’s rights in any patent claims that are infringed by (i) modifications 11 | to the Software made by you or any third party or (ii) the Software in 12 | combination with any software or other technology. 13 | 14 | The license granted hereunder will terminate, automatically and without notice, 15 | if you (or any of your subsidiaries, corporate affiliates or agents) initiate 16 | directly or indirectly, or take a direct financial interest in, any Patent 17 | Assertion: (i) against Facebook or any of its subsidiaries or corporate 18 | affiliates, (ii) against any party if such Patent Assertion arises in whole or 19 | in part from any software, technology, product or service of Facebook or any of 20 | its subsidiaries or corporate affiliates, or (iii) against any party relating 21 | to the Software. Notwithstanding the foregoing, if Facebook or any of its 22 | subsidiaries or corporate affiliates files a lawsuit alleging patent 23 | infringement against you in the first instance, and you respond by filing a 24 | patent infringement counterclaim in that lawsuit against that party that is 25 | unrelated to the Software, the license granted hereunder will not terminate 26 | under section (i) of this paragraph due to such counterclaim. 27 | 28 | A "Necessary Claim" is a claim of a patent owned by Facebook that is 29 | necessarily infringed by the Software standing alone. 30 | 31 | A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, 32 | or contributory infringement or inducement to infringe any patent, including a 33 | cross-claim or counterclaim. 34 | 35 | ----- 36 | 37 | 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. 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ## 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 ⚠️ 2 | 3 | Parse Command Line Tool 4 | ======= 5 | [](https://travis-ci.org/parse-community/parse-cli) 6 | [](https://community.parseplatform.org/c/parse-server) 7 | [][open-collective-link] 8 | [][open-collective-link] 9 | [![License][license-svg]][license-link] 10 | [](https://twitter.com/intent/follow?screen_name=ParsePlatform) 11 | 12 | The `Parse Command Line Tool` allows you to set up your Parse app's server-side code from the terminal. 13 | You can deploy your server-side code to either Parse Cloud Code or Heroku Node.js. 14 | 15 | Overview 16 | -------- 17 | Parse Command Line Tool can be used to perform various actions on your Parse app. 18 | It can be used to create new Parse apps, deploy Cloud Code to an app, view all releases for an app, etc. 19 | 20 | To install `Parse Command Line Tool`, you can just type the following command. 21 | 22 | NOTE: You should already have [Go](https://golang.org/doc/install) installed and GOPATH, GOROOT set to appropriate values. 23 | 24 | ```bash 25 | go get -t github.com/ParsePlatform/parse-cli 26 | ``` 27 | 28 | This installs a binary called `parse-cli` at `$GOPATH/bin`. 29 | Further, you can find the code for Parse CLI at: `$GOPATH/src/github.com/ParsePlatform/parse-cli`. 30 | 31 | The following commands are currently available in the `Parse Command Line Tool`: 32 | ```bash 33 | add Adds a new Parse app to config in current Cloud Code directory 34 | configure Configure various Parse settings 35 | default Sets or gets the default Parse app 36 | deploy Deploys a Parse app 37 | develop Monitors for changes to code and deploys, also tails parse logs 38 | download Downloads the Cloud Code project 39 | functions List Cloud Code functions and function webhooks 40 | generate Generates a sample express app in the current project directory 41 | jssdk Sets the Parse JavaScript SDK version to use in Cloud Code 42 | list Lists properties of the given Parse app and Parse apps associated with given project 43 | logs Prints out recent log messages 44 | migrate Migrate project config format to the preferred format 45 | new Adds Cloud Code to an existing Parse app, can also create a new Parse app 46 | releases Gets the releases for a Parse app 47 | rollback Rolls back the version for the given app 48 | symbols Uploads symbol files 49 | triggers List Cloud Code triggers and trigger webhooks 50 | update Updates this tool to the latest version 51 | version Gets the Command Line Tools version 52 | help Help about any command 53 | ``` 54 | 55 | ----- 56 | 57 | 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. 58 | 59 | [license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg 60 | [license-link]: LICENSE 61 | [open-collective-link]: https://opencollective.com/parse-server 62 | -------------------------------------------------------------------------------- /add.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ParsePlatform/parse-cli/herokucmd" 5 | "github.com/ParsePlatform/parse-cli/parsecli" 6 | "github.com/ParsePlatform/parse-cli/parsecmd" 7 | "github.com/facebookgo/stackerr" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type addCmd struct { 12 | MakeDefault bool 13 | apps *parsecli.Apps 14 | verbose bool 15 | } 16 | 17 | func (a *addCmd) addSelectedApp( 18 | name string, 19 | appConfig parsecli.AppConfig, 20 | args []string, 21 | e *parsecli.Env, 22 | ) error { 23 | switch e.Type { 24 | case parsecli.LegacyParseFormat, parsecli.ParseFormat: 25 | parseAppConfig, ok := appConfig.(*parsecli.ParseAppConfig) 26 | if !ok { 27 | return stackerr.New("invalid parse app config passed.") 28 | } 29 | return parsecmd.AddSelectedParseApp(name, parseAppConfig, args, a.MakeDefault, a.verbose, e) 30 | case parsecli.HerokuFormat: 31 | herokuAppConfig, ok := appConfig.(*parsecli.HerokuAppConfig) 32 | if !ok { 33 | return stackerr.New("invalid heroku app config passed.") 34 | } 35 | return herokucmd.AddSelectedHerokuApp(name, herokuAppConfig, args, a.MakeDefault, a.verbose, e) 36 | } 37 | 38 | return stackerr.Newf("Unknown project type: %d.", e.Type) 39 | } 40 | 41 | func (a *addCmd) selectApp(e *parsecli.Env, appName string) (*parsecli.App, error) { 42 | apps, err := a.apps.RestFetchApps(e) 43 | if err != nil { 44 | return nil, err 45 | } 46 | if appName != "" { 47 | for _, app := range apps { 48 | if app.Name == appName { 49 | return app, nil 50 | } 51 | } 52 | return nil, stackerr.Newf("You are not a collaborator on app: %s", appName) 53 | } 54 | 55 | app, err := a.apps.SelectApp(apps, "Select an App to add to config: ", e) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return app, nil 60 | } 61 | 62 | func (a *addCmd) run(e *parsecli.Env, args []string) error { 63 | 64 | if err := a.apps.Login.AuthUser(e, false); err != nil { 65 | return err 66 | } 67 | var appName string 68 | if len(args) > 1 { 69 | return stackerr.New("Only an optional Parse app name is expected.") 70 | } 71 | if len(args) == 1 { 72 | appName = args[0] 73 | } 74 | 75 | app, err := a.selectApp(e, appName) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | var appConfig parsecli.AppConfig 81 | switch e.Type { 82 | case parsecli.LegacyParseFormat, parsecli.ParseFormat: 83 | appConfig = parsecmd.GetParseAppConfig(app) 84 | 85 | case parsecli.HerokuFormat: 86 | _, appConfig, err = herokucmd.GetLinkedHerokuAppConfig(app, e) 87 | if err != nil { 88 | return err 89 | } 90 | } 91 | 92 | return a.addSelectedApp(app.Name, appConfig, args, e) 93 | } 94 | 95 | func NewAddCmd(e *parsecli.Env) *cobra.Command { 96 | a := &addCmd{ 97 | MakeDefault: false, 98 | apps: &parsecli.Apps{}, 99 | verbose: true, 100 | } 101 | cmd := &cobra.Command{ 102 | Use: "add [app]", 103 | Short: "Adds a new Parse App to config in current Cloud Code directory", 104 | Long: `Adds a new Parse App to config in current Cloud Code directory. 105 | If an argument is given, the added application can also be referenced by that name.`, 106 | Run: parsecli.RunWithArgs(e, a.run), 107 | } 108 | cmd.Flags().BoolVarP(&a.MakeDefault, "default", "d", a.MakeDefault, 109 | "Make the selected app default") 110 | return cmd 111 | } 112 | -------------------------------------------------------------------------------- /add_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/ParsePlatform/parse-cli/parsecli" 13 | "github.com/facebookgo/ensure" 14 | "github.com/facebookgo/parse" 15 | ) 16 | 17 | var ( 18 | defaultCredentials = parsecli.Credentials{Email: "email", Password: "password"} 19 | defaultTokenCredentials = parsecli.Credentials{Email: "email", Token: "token"} 20 | defaultApps = parsecli.Apps{Login: parsecli.Login{Credentials: defaultCredentials}} 21 | defaultAppsWithToken = parsecli.Apps{Login: parsecli.Login{Credentials: defaultTokenCredentials}} 22 | ) 23 | 24 | func jsonStr(t testing.TB, v interface{}) string { 25 | b, err := json.Marshal(v) 26 | ensure.Nil(t, err) 27 | return string(b) 28 | } 29 | 30 | func newAddCmdHarness(t testing.TB) (*parsecli.Harness, []*parsecli.App) { 31 | h := parsecli.NewHarness(t) 32 | defer h.Stop() 33 | 34 | apps := []*parsecli.App{ 35 | { 36 | Name: "A", 37 | ApplicationID: "appId.A", 38 | MasterKey: "masterKey.A", 39 | }, 40 | { 41 | Name: "B", 42 | ApplicationID: "appId.B", 43 | MasterKey: "masterKey.B", 44 | }, 45 | } 46 | res := map[string][]*parsecli.App{"results": apps} 47 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 48 | ensure.DeepEqual(t, r.URL.Path, "/1/apps") 49 | 50 | email := r.Header.Get("X-Parse-Email") 51 | password := r.Header.Get("X-Parse-Password") 52 | token := r.Header.Get("X-Parse-Account-Key") 53 | if !((email == "email" && password == "password") || (token == "token")) { 54 | return &http.Response{ 55 | StatusCode: http.StatusUnauthorized, 56 | Body: ioutil.NopCloser(strings.NewReader(`{"error": "incorrect credentials"}`)), 57 | }, nil 58 | } 59 | 60 | return &http.Response{ 61 | StatusCode: http.StatusOK, 62 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), 63 | }, nil 64 | 65 | }) 66 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 67 | return h, apps 68 | } 69 | 70 | func newAddCmdHarnessWithConfig(t testing.TB) (*parsecli.Harness, *addCmd, error) { 71 | a := addCmd{apps: &defaultApps} 72 | 73 | h, _ := newAddCmdHarness(t) 74 | h.MakeEmptyRoot() 75 | 76 | ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) 77 | 78 | return h, &a, nil 79 | } 80 | 81 | func TestAddCmdGlobalMakeDefault(t *testing.T) { 82 | t.Parallel() 83 | h, a, err := newAddCmdHarnessWithConfig(t) 84 | defer h.Stop() 85 | a.MakeDefault = true 86 | ensure.Nil(t, err) 87 | h.Env.Type = parsecli.ParseFormat 88 | h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) 89 | ensure.Nil(t, a.run(h.Env, []string{})) 90 | 91 | content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.ParseLocal)) 92 | ensure.Nil(t, err) 93 | ensure.DeepEqual(t, string(content), `{ 94 | "applications": { 95 | "A": { 96 | "applicationId": "appId.A" 97 | }, 98 | "_default": { 99 | "link": "A" 100 | } 101 | } 102 | }`) 103 | } 104 | 105 | func TestAddCmdGlobalNotDefault(t *testing.T) { 106 | t.Parallel() 107 | h, a, err := newAddCmdHarnessWithConfig(t) 108 | defer h.Stop() 109 | ensure.Nil(t, err) 110 | h.Env.Type = parsecli.ParseFormat 111 | h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) 112 | ensure.Nil(t, a.run(h.Env, []string{})) 113 | 114 | content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.ParseLocal)) 115 | ensure.Nil(t, err) 116 | ensure.DeepEqual(t, string(content), `{ 117 | "applications": { 118 | "A": { 119 | "applicationId": "appId.A" 120 | } 121 | } 122 | }`) 123 | } 124 | 125 | // NOTE: test functionality for legacy config format 126 | 127 | func newLegacyAddCmdHarnessWithConfig(t testing.TB) (*parsecli.Harness, *addCmd, error) { 128 | a := addCmd{apps: &defaultApps} 129 | 130 | h, _ := newAddCmdHarness(t) 131 | h.MakeEmptyRoot() 132 | 133 | ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) 134 | ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) 135 | ensure.Nil(t, 136 | ioutil.WriteFile( 137 | filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), 138 | []byte("{}"), 139 | 0600, 140 | ), 141 | ) 142 | return h, &a, nil 143 | } 144 | 145 | func TestLegacyAddCmdGlobalMakeDefault(t *testing.T) { 146 | t.Parallel() 147 | h, a, err := newLegacyAddCmdHarnessWithConfig(t) 148 | defer h.Stop() 149 | a.MakeDefault = true 150 | ensure.Nil(t, err) 151 | h.Env.Type = parsecli.LegacyParseFormat 152 | h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) 153 | ensure.Nil(t, a.run(h.Env, []string{})) 154 | 155 | content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.LegacyConfigFile)) 156 | ensure.Nil(t, err) 157 | ensure.DeepEqual(t, string(content), `{ 158 | "global": {}, 159 | "applications": { 160 | "A": { 161 | "applicationId": "appId.A" 162 | }, 163 | "_default": { 164 | "link": "A" 165 | } 166 | } 167 | }`) 168 | } 169 | 170 | func TestLegacyAddCmdGlobalNotDefault(t *testing.T) { 171 | t.Parallel() 172 | h, a, err := newLegacyAddCmdHarnessWithConfig(t) 173 | defer h.Stop() 174 | ensure.Nil(t, err) 175 | h.Env.Type = parsecli.LegacyParseFormat 176 | h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) 177 | ensure.Nil(t, a.run(h.Env, []string{})) 178 | 179 | content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.LegacyConfigFile)) 180 | ensure.Nil(t, err) 181 | ensure.DeepEqual(t, string(content), `{ 182 | "global": {}, 183 | "applications": { 184 | "A": { 185 | "applicationId": "appId.A" 186 | } 187 | } 188 | }`) 189 | } 190 | -------------------------------------------------------------------------------- /configure_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/ParsePlatform/parse-cli/parsecli" 11 | "github.com/facebookgo/ensure" 12 | ) 13 | 14 | func TestConfigureAccountKey(t *testing.T) { 15 | t.Parallel() 16 | 17 | h := parsecli.NewTokenHarness(t) 18 | defer h.Stop() 19 | 20 | c := configureCmd{login: parsecli.Login{TokenReader: strings.NewReader("")}} 21 | 22 | h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) 23 | ensure.Nil(t, c.accountKey(h.Env)) 24 | ensure.StringContains( 25 | t, 26 | h.Out.String(), 27 | ` 28 | Input your account key or press ENTER to generate a new one. 29 | `) 30 | 31 | h.Env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) 32 | ensure.Err(t, c.accountKey(h.Env), regexp.MustCompile("is not valid")) 33 | ensure.DeepEqual(t, 34 | h.Err.String(), 35 | "Could not store credentials. Please try again.\n\n", 36 | ) 37 | 38 | h.Env.Server = "http://api.parse.com/1/" 39 | c.tokenReader = strings.NewReader( 40 | `machine api.parse.com#email 41 | login default 42 | password token2 43 | `, 44 | ) 45 | h.Err.Reset() 46 | h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) 47 | ensure.Nil(t, c.accountKey(h.Env)) 48 | ensure.DeepEqual(t, h.Err.String(), 49 | `Note: this operation will overwrite the account key: 50 | "*oken" 51 | for email: "email" 52 | `) 53 | 54 | h.Env.Server = "http://api.parse.com/1/" 55 | c.tokenReader = strings.NewReader( 56 | `machine api.parse.com#email 57 | login default 58 | password token2 59 | `, 60 | ) 61 | c.isDefault = true 62 | h.Err.Reset() 63 | h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) 64 | ensure.Nil(t, c.accountKey(h.Env)) 65 | ensure.DeepEqual(t, h.Err.String(), "") 66 | 67 | h.Env.Server = "http://api.parse.com/1/" 68 | c.tokenReader = strings.NewReader( 69 | `machine api.parse.com 70 | login default 71 | password token2 72 | `, 73 | ) 74 | c.isDefault = true 75 | h.Err.Reset() 76 | h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) 77 | ensure.Nil(t, c.accountKey(h.Env)) 78 | ensure.DeepEqual(t, h.Err.String(), "Note: this operation will overwrite the default account key\n") 79 | 80 | h.Env.Server = "http://api.parse.com/1/" 81 | c.tokenReader = strings.NewReader( 82 | `machine api.parse.com 83 | login default 84 | password token2 85 | `, 86 | ) 87 | h.Err.Reset() 88 | c.isDefault = false 89 | h.Env.In = ioutil.NopCloser(strings.NewReader("token\n")) 90 | ensure.Nil(t, c.accountKey(h.Env)) 91 | ensure.DeepEqual(t, h.Err.String(), "") 92 | } 93 | 94 | func TestParserEmail(t *testing.T) { 95 | t.Parallel() 96 | 97 | h := parsecli.NewTokenHarness(t) 98 | h.MakeEmptyRoot() 99 | defer h.Stop() 100 | 101 | ensure.Nil(t, parsecli.CreateConfigWithContent(filepath.Join(h.Env.Root, parsecli.ParseLocal), "{}")) 102 | ensure.Nil(t, 103 | parsecli.CreateConfigWithContent( 104 | filepath.Join(h.Env.Root, parsecli.ParseProject), 105 | `{"project_type": 1}`, 106 | ), 107 | ) 108 | 109 | var c configureCmd 110 | ensure.Nil(t, c.parserEmail(h.Env, []string{"email2"})) 111 | ensure.DeepEqual( 112 | t, 113 | h.Out.String(), 114 | `Successfully configured email for current project to: "email2" 115 | `, 116 | ) 117 | 118 | ensure.Err(t, c.parserEmail(h.Env, nil), regexp.MustCompile("Invalid args:")) 119 | ensure.Err(t, c.parserEmail(h.Env, []string{"a", "b"}), regexp.MustCompile("Invalid args:")) 120 | } 121 | 122 | func TestProjectType(t *testing.T) { 123 | t.Parallel() 124 | h := parsecli.NewHarness(t) 125 | defer h.Stop() 126 | 127 | h.MakeEmptyRoot() 128 | ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, false)) 129 | 130 | c := &configureCmd{} 131 | err := c.projectType(h.Env, []string{"1", "2"}) 132 | ensure.Err(t, err, regexp.MustCompile("only an optional project type argument is expected")) 133 | 134 | h.Env.In = ioutil.NopCloser(strings.NewReader("invalid\n")) 135 | err = c.projectType(h.Env, nil) 136 | ensure.StringContains(t, h.Err.String(), "Invalid selection. Please enter a number") 137 | ensure.Err(t, err, regexp.MustCompile("Could not make a selection. Please try again.")) 138 | h.Err.Reset() 139 | h.Out.Reset() 140 | 141 | h.Env.In = ioutil.NopCloser(strings.NewReader("0\n")) 142 | err = c.projectType(h.Env, nil) 143 | ensure.StringContains(t, h.Err.String(), "Please enter a number between 1 and") 144 | ensure.Err(t, err, regexp.MustCompile("Could not make a selection. Please try again.")) 145 | h.Err.Reset() 146 | h.Out.Reset() 147 | 148 | h.Env.In = ioutil.NopCloser(strings.NewReader("1\n")) 149 | err = c.projectType(h.Env, nil) 150 | ensure.StringContains(t, h.Out.String(), "Successfully set project type to: parse") 151 | ensure.Nil(t, err) 152 | } 153 | -------------------------------------------------------------------------------- /default.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/ParsePlatform/parse-cli/parsecli" 5 | "github.com/facebookgo/stackerr" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type defaultCmd struct{} 10 | 11 | func (d *defaultCmd) run(e *parsecli.Env, args []string) error { 12 | var newDefault string 13 | if len(args) > 1 { 14 | return stackerr.Newf("unexpected arguments, only an optional app name is expected: %v", args) 15 | } 16 | if len(args) == 1 { 17 | newDefault = args[0] 18 | } 19 | 20 | config, err := parsecli.ConfigFromDir(e.Root) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if config.GetNumApps() == 0 { 26 | return stackerr.New("No apps are associated with this project. You can add some with parse add") 27 | } 28 | 29 | defaultApp := config.GetDefaultApp() 30 | 31 | switch newDefault { 32 | case "": 33 | return parsecli.PrintDefault(e, defaultApp) 34 | default: 35 | return parsecli.SetDefault(e, newDefault, defaultApp, config) 36 | } 37 | } 38 | 39 | func NewDefaultCmd(e *parsecli.Env) *cobra.Command { 40 | d := defaultCmd{} 41 | return &cobra.Command{ 42 | Use: "default [app]", 43 | Short: "Sets or gets the default Parse App", 44 | Long: `Gets the default Parse App. If an argument is given, sets the ` + 45 | `default Parse App.`, 46 | Run: parsecli.RunWithArgs(e, d.run), 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /default_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "testing" 9 | 10 | "github.com/ParsePlatform/parse-cli/parsecli" 11 | "github.com/facebookgo/ensure" 12 | ) 13 | 14 | func newDefaultParseConfig(t testing.TB, h *parsecli.Harness) *parsecli.ParseConfig { 15 | c, err := parsecli.ConfigFromDir(h.Env.Root) 16 | ensure.Nil(t, err) 17 | 18 | config, ok := c.(*parsecli.ParseConfig) 19 | ensure.True(t, ok) 20 | config.Applications = map[string]*parsecli.ParseAppConfig{ 21 | "first": {}, 22 | "second": {}, 23 | parsecli.DefaultKey: {Link: "first"}, 24 | } 25 | config.ProjectConfig = &parsecli.ProjectConfig{ 26 | Type: parsecli.LegacyParseFormat, 27 | Parse: &parsecli.ParseProjectConfig{}, 28 | } 29 | ensure.Nil(t, parsecli.StoreConfig(h.Env, config)) 30 | 31 | return config 32 | } 33 | 34 | func newDefaultCmdHarness(t testing.TB) (*parsecli.Harness, *defaultCmd, *parsecli.ParseConfig) { 35 | h := parsecli.NewHarness(t) 36 | h.MakeEmptyRoot() 37 | 38 | ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) 39 | 40 | config := newDefaultParseConfig(t, h) 41 | h.Out.Reset() 42 | 43 | d := &defaultCmd{} 44 | return h, d, config 45 | } 46 | 47 | func TestPrintDefaultNotSet(t *testing.T) { 48 | t.Parallel() 49 | h, _, _ := newDefaultCmdHarness(t) 50 | defer h.Stop() 51 | ensure.Err(t, parsecli.PrintDefault(h.Env, ""), regexp.MustCompile("No app is set as default app")) 52 | } 53 | 54 | func TestPrintDefaultSet(t *testing.T) { 55 | t.Parallel() 56 | h, _, _ := newDefaultCmdHarness(t) 57 | defer h.Stop() 58 | parsecli.PrintDefault(h.Env, "first") 59 | ensure.DeepEqual(t, h.Out.String(), 60 | `Current default app is first 61 | `) 62 | } 63 | 64 | func TestSetDefaultInvalid(t *testing.T) { 65 | t.Parallel() 66 | h, _, config := newDefaultCmdHarness(t) 67 | defer h.Stop() 68 | ensure.Err( 69 | t, 70 | parsecli.SetDefault(h.Env, "invalid", "", config), 71 | regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`), 72 | ) 73 | ensure.DeepEqual(t, h.Out.String(), 74 | `The following apps are associated with Cloud Code in the current directory: 75 | * first 76 | second 77 | `) 78 | } 79 | 80 | func TestSetDefault(t *testing.T) { 81 | t.Parallel() 82 | h, _, config := newDefaultCmdHarness(t) 83 | defer h.Stop() 84 | ensure.Nil(t, parsecli.SetDefault(h.Env, "first", "", config)) 85 | ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") 86 | } 87 | 88 | func TestSetDefaultWithApp(t *testing.T) { 89 | t.Parallel() 90 | h, _, config := newDefaultCmdHarness(t) 91 | defer h.Stop() 92 | ensure.Nil(t, parsecli.SetDefault(h.Env, "first", "first", config)) 93 | ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") 94 | } 95 | 96 | func TestDefaultRunMoreArgs(t *testing.T) { 97 | t.Parallel() 98 | h, d, _ := newDefaultCmdHarness(t) 99 | defer h.Stop() 100 | ensure.Err(t, d.run(h.Env, []string{"one", "two"}), regexp.MustCompile(`unexpected arguments, only an optional app name is expected:`)) 101 | } 102 | 103 | // NOTE: testing for legacy format 104 | func newLegacyDefaultCmdHarness(t testing.TB) (*parsecli.Harness, *defaultCmd, *parsecli.ParseConfig) { 105 | h := parsecli.NewHarness(t) 106 | h.MakeEmptyRoot() 107 | 108 | ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) 109 | ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) 110 | ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), 111 | []byte("{}"), 112 | 0600), 113 | ) 114 | 115 | config := newDefaultParseConfig(t, h) 116 | h.Out.Reset() 117 | 118 | d := &defaultCmd{} 119 | return h, d, config 120 | } 121 | 122 | func TestLegacySetDefaultInvalid(t *testing.T) { 123 | t.Parallel() 124 | h, _, config := newLegacyDefaultCmdHarness(t) 125 | defer h.Stop() 126 | ensure.Err( 127 | t, 128 | parsecli.SetDefault(h.Env, "invalid", "", config), 129 | regexp.MustCompile(`Invalid application name "invalid". Please select from the valid applications printed above.`), 130 | ) 131 | ensure.DeepEqual(t, h.Out.String(), 132 | `The following apps are associated with Cloud Code in the current directory: 133 | * first 134 | second 135 | `) 136 | } 137 | 138 | func TestLegacySetDefault(t *testing.T) { 139 | t.Parallel() 140 | h, _, config := newLegacyDefaultCmdHarness(t) 141 | defer h.Stop() 142 | ensure.Nil(t, parsecli.SetDefault(h.Env, "first", "", config)) 143 | ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") 144 | } 145 | 146 | func TestLegacySetDefaultWithApp(t *testing.T) { 147 | t.Parallel() 148 | h, _, config := newLegacyDefaultCmdHarness(t) 149 | defer h.Stop() 150 | ensure.Nil(t, parsecli.SetDefault(h.Env, "first", "first", config)) 151 | ensure.DeepEqual(t, h.Out.String(), "Default app set to first.\n") 152 | } 153 | -------------------------------------------------------------------------------- /heroku_commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/ParsePlatform/parse-cli/herokucmd" 9 | "github.com/ParsePlatform/parse-cli/parsecli" 10 | "github.com/ParsePlatform/parse-cli/webhooks" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func herokuRootCmd(e *parsecli.Env) ([]string, *cobra.Command) { 15 | c := &cobra.Command{ 16 | Use: "parse", 17 | Long: fmt.Sprintf( 18 | `Parse Command Line Tool 19 | 20 | Tools to help you set up server code on Heroku 21 | Version %v 22 | Copyright %d Parse, Inc. 23 | https://www.parse.com`, 24 | parsecli.Version, 25 | time.Now().Year(), 26 | ), 27 | Run: func(cmd *cobra.Command, args []string) { 28 | cmd.Help() 29 | }, 30 | } 31 | 32 | c.AddCommand(NewAddCmd(e)) 33 | c.AddCommand(NewConfigureCmd(e)) 34 | c.AddCommand(NewDefaultCmd(e)) 35 | c.AddCommand(herokucmd.NewDownloadCmd(e)) 36 | c.AddCommand(herokucmd.NewDeployCmd(e)) 37 | c.AddCommand(webhooks.NewFunctionHooksCmd(e)) 38 | c.AddCommand(NewListCmd(e)) 39 | c.AddCommand(herokucmd.NewLogsCmd(e)) 40 | c.AddCommand(NewNewCmd(e)) 41 | c.AddCommand(herokucmd.NewReleasesCmd(e)) 42 | c.AddCommand(herokucmd.NewRollbackCmd(e)) 43 | c.AddCommand(webhooks.NewTriggerHooksCmd(e)) 44 | c.AddCommand(NewUpdateCmd(e)) 45 | c.AddCommand(NewVersionCmd(e)) 46 | 47 | if len(os.Args) <= 1 { 48 | return nil, c 49 | } 50 | commands := []string{"help"} 51 | for _, command := range c.Commands() { 52 | commands = append(commands, command.Name()) 53 | } 54 | 55 | args := make([]string, len(os.Args)-1) 56 | copy(args, os.Args[1:]) 57 | 58 | if message := parsecli.MakeCorrections(commands, args); message != "" { 59 | fmt.Fprintln(e.Out, message) 60 | } 61 | c.SetArgs(args) 62 | 63 | return args, c 64 | } 65 | -------------------------------------------------------------------------------- /herokucmd/add_test.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ParsePlatform/parse-cli/parsecli" 7 | "github.com/facebookgo/ensure" 8 | ) 9 | 10 | func TestGetRandomAppName(t *testing.T) { 11 | app := &parsecli.App{Name: "test app", ApplicationID: "123456789"} 12 | name := getRandomAppName(app) 13 | ensure.StringContains(t, name, "testapp") 14 | } 15 | -------------------------------------------------------------------------------- /herokucmd/deploy.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/ParsePlatform/parse-cli/parsecli" 9 | "github.com/ParsePlatform/parse-cli/webhooks" 10 | "github.com/bgentry/heroku-go" 11 | "github.com/facebookgo/stackerr" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type deployCmd struct { 16 | Force bool 17 | Description string 18 | } 19 | 20 | func fetchHerokuAppInfo(e *parsecli.Env, appConfig *parsecli.HerokuAppConfig) (*heroku.App, error) { 21 | herokuAppInfo, err := e.HerokuAPIClient.AppInfo(appConfig.HerokuAppID) 22 | if err != nil { 23 | return nil, stackerr.Newf( 24 | "Unable to fetch app info for: %q from heroku.", 25 | appConfig.HerokuAppID, 26 | ) 27 | } 28 | return herokuAppInfo, nil 29 | } 30 | 31 | func setHerokuConfigs( 32 | e *parsecli.Env, 33 | appConfig *parsecli.HerokuAppConfig, 34 | options map[string]*string, 35 | ) error { 36 | varsSet, err := e.HerokuAPIClient.ConfigVarUpdate( 37 | appConfig.HerokuAppID, 38 | options, 39 | ) 40 | if err != nil { 41 | stackerr.New("Unable to set config vars to heroku.") 42 | } 43 | fmt.Fprintln(e.Out, "Successfully set the following vars to heroku:") 44 | for key, val := range varsSet { 45 | if key != "HOOKS_URL" { 46 | val = parsecli.Last4(val) 47 | } 48 | fmt.Fprintln(e.Out, key, val) 49 | } 50 | return nil 51 | } 52 | 53 | func (d *deployCmd) deploy(e *parsecli.Env, ctx *parsecli.Context) error { 54 | appConfig, ok := ctx.AppConfig.(*parsecli.HerokuAppConfig) 55 | if !ok { 56 | return stackerr.New("Invalid config type.") 57 | } 58 | 59 | authToken, err := ctx.AppConfig.GetApplicationAuth(e) 60 | if err != nil { 61 | return err 62 | } 63 | appInfo, err := fetchHerokuAppInfo(e, appConfig) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | // set appropriate heroku config vars 69 | appKeys, err := parsecli.FetchAppKeys(e, appConfig.ParseAppID) 70 | if err != nil { 71 | return err 72 | } 73 | fmt.Fprintln(e.Out, "This Node.js webhooks project will be deployed to Heroku.") 74 | err = setHerokuConfigs(e, 75 | appConfig, 76 | map[string]*string{ 77 | "HOOKS_URL": &appInfo.WebURL, 78 | "PARSE_APP_ID": &appConfig.ParseAppID, 79 | "PARSE_MASTER_KEY": &appKeys.MasterKey, 80 | "PARSE_WEBHOOK_KEY": &appKeys.WebhookKey, 81 | }, 82 | ) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | // push to heroku git 88 | g := gitInfo{ 89 | repo: e.Root, 90 | authToken: authToken, 91 | remote: fmt.Sprintf("git.heroku.com/%s.git", appInfo.Name), 92 | description: d.Description, 93 | } 94 | err = g.whichGit() 95 | if err != nil { 96 | return err 97 | } 98 | err = g.isGitRepo(e) 99 | if err != nil { 100 | err = g.init(e) 101 | if err != nil { 102 | return err 103 | } 104 | } 105 | dirty, err := g.isDirty(e) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | if dirty { 111 | err := g.commit(e) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | if err := g.push(e, d.Force); err != nil { 117 | return err 118 | } 119 | 120 | webhooksConfig := filepath.Join(e.Root, "webhooks.json") 121 | if _, err := os.Lstat(webhooksConfig); err == nil { 122 | h := &webhooks.Hooks{BaseURL: appInfo.WebURL} 123 | return h.HooksCmd(e, ctx, []string{webhooksConfig}) 124 | } 125 | 126 | return nil 127 | } 128 | 129 | func NewDeployCmd(e *parsecli.Env) *cobra.Command { 130 | var d deployCmd 131 | cmd := &cobra.Command{ 132 | Use: "deploy [app]", 133 | Short: "Deploys server code to Heroku", 134 | Long: "Deploys server code to Heroku.", 135 | Run: parsecli.RunWithClient(e, d.deploy), 136 | } 137 | cmd.Flags().BoolVarP(&d.Force, "force", "f", d.Force, 138 | "Forcefully deploy current contents even if they differ hugely from existing deploy.") 139 | cmd.Flags().StringVarP(&d.Description, "description", "d", d.Description, 140 | "Description is used as git commit message, if repo is dirty, before pushing to Heroku remote.") 141 | return cmd 142 | } 143 | -------------------------------------------------------------------------------- /herokucmd/download.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ParsePlatform/parse-cli/parsecli" 7 | "github.com/facebookgo/stackerr" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | type downloadCmd struct{} 12 | 13 | func (h *downloadCmd) run(e *parsecli.Env, ctx *parsecli.Context) error { 14 | appConfig, ok := ctx.AppConfig.(*parsecli.HerokuAppConfig) 15 | if !ok { 16 | return stackerr.New("expected heroku project config type") 17 | } 18 | 19 | authToken, err := appConfig.GetApplicationAuth(e) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | herokuAppName, err := parsecli.FetchHerokuAppName(appConfig.HerokuAppID, e) 25 | if err != nil { 26 | return err 27 | } 28 | gitURL := fmt.Sprintf("https://:%s@git.heroku.com/%s.git", authToken, herokuAppName) 29 | return (&gitInfo{}).pull(e, gitURL) 30 | } 31 | 32 | func NewDownloadCmd(e *parsecli.Env) *cobra.Command { 33 | d := &downloadCmd{} 34 | cmd := &cobra.Command{ 35 | Use: "download", 36 | Short: "Downloads the latest deployed server code from Heroku servers", 37 | Long: "Downloads the latest deployed server code from Heroku servers.", 38 | Run: parsecli.RunWithClient(e, d.run), 39 | } 40 | return cmd 41 | } 42 | -------------------------------------------------------------------------------- /herokucmd/git.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | "time" 11 | 12 | "github.com/ParsePlatform/parse-cli/parsecli" 13 | "github.com/facebookgo/stackerr" 14 | ) 15 | 16 | type gitInfo struct { 17 | repo string 18 | authToken string 19 | remote string 20 | description string 21 | } 22 | 23 | func (g *gitInfo) whichGit() error { 24 | _, err := exec.LookPath("git") 25 | if err != nil { 26 | return stackerr.New(`Unable to locate "git". 27 | Please install "git" and ensure that you are able to run "git help" from the command prompt.`, 28 | ) 29 | } 30 | return nil 31 | } 32 | 33 | func (g *gitInfo) isGitRepo(e *parsecli.Env) error { 34 | _, err := os.Lstat(filepath.Join(e.Root, ".git")) 35 | if os.IsNotExist(err) { 36 | return stackerr.Newf("%s is not a git repository. Please run 'git init'", e.Root) 37 | } 38 | return nil 39 | } 40 | 41 | func (g *gitInfo) checkAbort(e *parsecli.Env) error { 42 | err := g.whichGit() 43 | if err != nil { 44 | return err 45 | } 46 | return g.isGitRepo(e) 47 | } 48 | 49 | func (g *gitInfo) runCmd(cmd *exec.Cmd, msg string) error { 50 | err := cmd.Run() 51 | if err == nil { 52 | return nil 53 | } 54 | if _, ok := err.(*exec.ExitError); ok { 55 | if msg != "" { 56 | return stackerr.New(msg) 57 | } 58 | return stackerr.Newf("error executing: %q", strings.Join(cmd.Args, " ")) 59 | } 60 | return stackerr.Wrap(err) 61 | } 62 | 63 | func (g *gitInfo) getDescription() string { 64 | if g.description != "" { 65 | return g.description 66 | } 67 | return fmt.Sprintf("committed by parse-cli at: %s", time.Now()) 68 | } 69 | 70 | func (g *gitInfo) isDirty(e *parsecli.Env) (bool, error) { 71 | if err := g.checkAbort(e); err != nil { 72 | return false, err 73 | } 74 | 75 | cmd := exec.Command("git", "status", "--porcelain") 76 | var cmdOut, cmdErr bytes.Buffer 77 | cmd.Stdout = &cmdOut 78 | cmd.Stderr = &cmdErr 79 | err := g.runCmd(cmd, fmt.Sprintf("Unable to determine the status of git repo: %s", g.repo)) 80 | if err != nil { 81 | fmt.Fprintln(e.Err, cmdErr.String()) 82 | return false, err 83 | } 84 | 85 | if cmdOut.Len() == 0 { 86 | return false, nil 87 | } 88 | 89 | fmt.Fprintf(e.Out, "Status of git repo: %q:\n%s", g.repo, cmdOut.String()) 90 | return true, nil 91 | } 92 | 93 | func (g *gitInfo) init(e *parsecli.Env) error { 94 | err := g.whichGit() 95 | if err != nil { 96 | return err 97 | } 98 | 99 | cmd := exec.Command("git", "init") 100 | cmd.Stdout = os.Stdout 101 | cmd.Stderr = os.Stderr 102 | return g.runCmd(cmd, "") 103 | } 104 | 105 | func (g *gitInfo) commit(e *parsecli.Env) error { 106 | if err := g.checkAbort(e); err != nil { 107 | return err 108 | } 109 | 110 | cmd := exec.Command("git", "add", "-A", ".") 111 | cmd.Stdout = os.Stdout 112 | cmd.Stderr = os.Stderr 113 | err := g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | cmd = exec.Command("git", "commit", "-a", "-m", g.getDescription()) 119 | return g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) 120 | } 121 | 122 | func (g *gitInfo) push(e *parsecli.Env, force bool) error { 123 | if err := g.checkAbort(e); err != nil { 124 | return err 125 | } 126 | command := []string{"push"} 127 | if force { 128 | command = append(command, "-f") 129 | } 130 | command = append(command, 131 | fmt.Sprintf("https://:%s@%s", g.authToken, g.remote), 132 | "master", 133 | ) 134 | cmd := exec.Command("git", command...) 135 | cmd.Stdout = os.Stdout 136 | cmd.Stderr = os.Stderr 137 | return g.runCmd( 138 | cmd, 139 | fmt.Sprintf("Unable to push to git remote: %s", g.remote), 140 | ) 141 | } 142 | 143 | func (g *gitInfo) isEmptyRepository(path string) (bool, error) { 144 | var files bytes.Buffer 145 | cmd := exec.Command("git", "-C", path, "ls-files") 146 | cmd.Stdout = &files 147 | cmd.Stderr = os.Stderr 148 | if err := g.runCmd(cmd, ""); err != nil { 149 | return false, err 150 | } 151 | return files.Len() == 0, nil 152 | } 153 | 154 | func (g *gitInfo) pull(e *parsecli.Env, gitURL string) error { 155 | if err := g.checkAbort(e); err != nil { 156 | return err 157 | } 158 | pullCmd := exec.Command("git", "pull", gitURL) 159 | pullCmd.Stdout = os.Stdout 160 | pullCmd.Stdin = os.Stdin 161 | return g.runCmd(pullCmd, "") 162 | } 163 | 164 | func (g *gitInfo) clone(gitURL, path string) error { 165 | _, err := exec.LookPath("git") 166 | if err != nil { 167 | return stackerr.New(`Unable to locate "git". 168 | Please install "git" and ensure that you are able to run "git help" from the command prompt.`, 169 | ) 170 | } 171 | 172 | cmd := exec.Command("git", "clone", gitURL, path) 173 | cmd.Stderr = os.Stderr 174 | return g.runCmd(cmd, fmt.Sprintf("error executing: %s", strings.Join(cmd.Args, " "))) 175 | } 176 | -------------------------------------------------------------------------------- /herokucmd/logs.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/ParsePlatform/parse-cli/parsecli" 8 | "github.com/bgentry/heroku-go" 9 | "github.com/facebookgo/stackerr" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type logsCmd struct { 14 | tail bool 15 | num int 16 | } 17 | 18 | func (h *logsCmd) run(e *parsecli.Env, ctx *parsecli.Context) error { 19 | hkConfig, ok := ctx.AppConfig.(*parsecli.HerokuAppConfig) 20 | if !ok { 21 | return stackerr.New("Unexpected config format") 22 | } 23 | 24 | opts := &heroku.LogSessionCreateOpts{} 25 | if h.num == 0 { 26 | h.num = 50 27 | } 28 | //source := "app" 29 | //opts.Source = &source 30 | opts.Lines = &h.num 31 | opts.Tail = &h.tail 32 | 33 | session, err := e.HerokuAPIClient.LogSessionCreate( 34 | hkConfig.HerokuAppID, 35 | opts, 36 | ) 37 | if err != nil { 38 | return stackerr.Wrap(err) 39 | } 40 | resp, err := http.Get(session.LogplexURL) 41 | if err != nil { 42 | return stackerr.Wrap(err) 43 | } 44 | _, err = io.Copy(e.Out, resp.Body) 45 | return stackerr.Wrap(err) 46 | } 47 | 48 | func NewLogsCmd(e *parsecli.Env) *cobra.Command { 49 | h := &logsCmd{num: 50} 50 | cmd := &cobra.Command{ 51 | Use: "logs", 52 | Short: "Fetch logs from heroku", 53 | Long: "Fetch logs from heroku.", 54 | Run: parsecli.RunWithClient(e, h.run), 55 | } 56 | cmd.Flags().BoolVarP(&h.tail, "follow", "f", h.tail, "Tail logs from server.") 57 | cmd.Flags().IntVarP(&h.num, "num", "n", h.num, "Number of log lines to fetch.") 58 | return cmd 59 | } 60 | -------------------------------------------------------------------------------- /herokucmd/new.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/ParsePlatform/parse-cli/parsecli" 13 | "github.com/facebookgo/stackerr" 14 | ) 15 | 16 | const ( 17 | nodeSampleURL = "https://github.com/pavanka/parse-webhooks/releases/download/3.0.0/node-sample.zip" 18 | nodeSampleBase = "node-sample" 19 | ) 20 | 21 | func recordDecision(e *parsecli.Env, decision string) { 22 | v := make(url.Values) 23 | v.Set("version", parsecli.Version) 24 | v.Set("decision", decision) 25 | req := &http.Request{ 26 | Method: "GET", 27 | URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, 28 | } 29 | e.ParseAPIClient.Do(req, nil, nil) 30 | } 31 | 32 | func PromptCreateWebhooks(e *parsecli.Env) (string, error) { 33 | selections := map[int]string{ 34 | 1: "Heroku (https://www.heroku.com)", 35 | 2: "Parse (https://parse.com/docs/cloudcode/guide)", 36 | } 37 | 38 | msg := "Sorry! CLI supports only Parse Cloud Code and Heroku." 39 | 40 | projectType := 0 41 | for i := 0; i < 3; i++ { 42 | fmt.Fprintf( 43 | e.Out, 44 | `Which of these providers would you like use for running your server code: 45 | %d) %s 46 | %d) %s 47 | Type 1 or 2 to make a selection: `, 48 | 1, selections[1], 49 | 2, selections[2], 50 | ) 51 | fmt.Fscanf(e.In, "%d\n", &projectType) 52 | fmt.Fprintln(e.Out) 53 | switch projectType { 54 | case 1: 55 | recordDecision(e, "heroku") 56 | return "heroku", nil 57 | case 2: 58 | recordDecision(e, "parse") 59 | return "parse", nil 60 | } 61 | fmt.Fprintln(e.Err, msg) 62 | } 63 | return "", stackerr.New(msg) 64 | } 65 | 66 | func writeHerokuConfigs(e *parsecli.Env) error { 67 | err := parsecli.CreateConfigWithContent( 68 | filepath.Join(e.Root, parsecli.ParseProject), 69 | fmt.Sprintf( 70 | `{"project_type": %d}`, 71 | parsecli.HerokuFormat, 72 | ), 73 | ) 74 | if err != nil { 75 | return err 76 | } 77 | return parsecli.CreateConfigWithContent( 78 | filepath.Join(e.Root, parsecli.ParseLocal), 79 | "{}", 80 | ) 81 | } 82 | 83 | func downloadNodeSample(e *parsecli.Env) (string, error) { 84 | tmpZip, err := ioutil.TempFile("", "parse-node-sample") 85 | if err != nil { 86 | return "", stackerr.Wrap(err) 87 | } 88 | 89 | resp, err := http.Get(nodeSampleURL) 90 | if err != nil { 91 | return "", stackerr.Wrap(err) 92 | } 93 | 94 | _, err = io.Copy(tmpZip, resp.Body) 95 | if err != nil { 96 | return "", stackerr.Wrap(err) 97 | } 98 | return tmpZip.Name(), nil 99 | } 100 | 101 | func setupNodeSample(e *parsecli.Env, dumpTemplate bool) error { 102 | if !dumpTemplate { 103 | err := os.MkdirAll(e.Root, 0755) 104 | if err != nil { 105 | return stackerr.Wrap(err) 106 | } 107 | return writeHerokuConfigs(e) 108 | } 109 | 110 | sampleCode, err := downloadNodeSample(e) 111 | if err != nil { 112 | return err 113 | } 114 | defer os.RemoveAll(sampleCode) 115 | 116 | err = unzip(sampleCode, nodeSampleBase, e.Root) 117 | if err != nil { 118 | return err 119 | } 120 | return writeHerokuConfigs(e) 121 | } 122 | 123 | func CloneNodeCode(e *parsecli.Env, isNew, onlyConfig bool, appConfig parsecli.AppConfig) (bool, error) { 124 | cloneTemplate := false 125 | if !isNew && !onlyConfig { 126 | authToken, err := appConfig.GetApplicationAuth(e) 127 | if err != nil { 128 | return false, err 129 | } 130 | herokuAppConfig, ok := appConfig.(*parsecli.HerokuAppConfig) 131 | if !ok { 132 | return false, stackerr.New("invalid heroku app config") 133 | } 134 | 135 | var gitURL string 136 | g := &gitInfo{} 137 | 138 | herokuAppName, err := parsecli.FetchHerokuAppName(herokuAppConfig.HerokuAppID, e) 139 | if err != nil { 140 | return false, err 141 | } 142 | gitURL = fmt.Sprintf("https://:%s@git.heroku.com/%s.git", authToken, herokuAppName) 143 | err = g.clone(gitURL, e.Root) 144 | if err != nil { 145 | fmt.Fprintf(e.Err, `Failed to fetch the latest deployed code from Heroku. 146 | Please try "git clone %s %s". 147 | Currently cloning the template project. 148 | `, 149 | gitURL, 150 | e.Root, 151 | ) 152 | cloneTemplate = true 153 | } else { 154 | isEmpty, err := g.isEmptyRepository(e.Root) 155 | if err != nil { 156 | return false, err 157 | } 158 | if isEmpty { 159 | if err := os.RemoveAll(e.Root); err != nil { 160 | return false, stackerr.Wrap(err) 161 | } 162 | cloneTemplate = true 163 | } 164 | } 165 | } 166 | cloneTemplate = (isNew || cloneTemplate) && !onlyConfig 167 | return cloneTemplate, setupNodeSample(e, cloneTemplate) 168 | } 169 | -------------------------------------------------------------------------------- /herokucmd/releases.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "fmt" 5 | "text/tabwriter" 6 | 7 | "github.com/ParsePlatform/parse-cli/parsecli" 8 | "github.com/bgentry/heroku-go" 9 | "github.com/facebookgo/stackerr" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type releasesCmd struct{} 14 | 15 | func (r *releasesCmd) printHerokuReleases(releases []heroku.Release, e *parsecli.Env) { 16 | w := new(tabwriter.Writer) 17 | w.Init(e.Out, 20, 4, 2, ' ', 0) 18 | fmt.Fprintln(w, "Version\tUpdate At\tDescription") 19 | for _, release := range releases { 20 | fmt.Fprintf( 21 | w, 22 | "%d\t%s\tDeployed by: %q. %s\n\n", 23 | release.Version, 24 | release.UpdatedAt, 25 | release.User.Email, 26 | release.Description, 27 | ) 28 | } 29 | w.Flush() 30 | } 31 | 32 | func (r *releasesCmd) run(e *parsecli.Env, c *parsecli.Context) error { 33 | appConfig, ok := c.AppConfig.(*parsecli.HerokuAppConfig) 34 | if !ok { 35 | return stackerr.New("Invalid app config.") 36 | } 37 | lr := &heroku.ListRange{Field: "version", Max: 10, Descending: true} 38 | releases, err := e.HerokuAPIClient.ReleaseList(appConfig.HerokuAppID, lr) 39 | if err != nil { 40 | return stackerr.Wrap(err) 41 | } 42 | r.printHerokuReleases(releases, e) 43 | return nil 44 | } 45 | 46 | func NewReleasesCmd(e *parsecli.Env) *cobra.Command { 47 | r := &releasesCmd{} 48 | cmd := &cobra.Command{ 49 | Use: "releases [app]", 50 | Short: "Gets the Heroku releases for a Parse app", 51 | Long: "Prints the last 10 releases made on Heroku", 52 | Run: parsecli.RunWithClient(e, r.run), 53 | } 54 | return cmd 55 | } 56 | -------------------------------------------------------------------------------- /herokucmd/rollback.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ParsePlatform/parse-cli/parsecli" 7 | "github.com/bgentry/heroku-go" 8 | "github.com/facebookgo/stackerr" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type rollbackCmd struct { 13 | ReleaseName string 14 | } 15 | 16 | func (r *rollbackCmd) getLastSecondVersion(e *parsecli.Env, name string) (int, error) { 17 | releases, err := e.HerokuAPIClient.ReleaseList(name, &heroku.ListRange{Field: "version", Max: 2, Descending: true}) 18 | if err != nil { 19 | return 0, stackerr.Wrap(err) 20 | } 21 | return releases[1].Version, nil 22 | } 23 | 24 | func (r *rollbackCmd) run(e *parsecli.Env, c *parsecli.Context) error { 25 | appConfig, ok := c.AppConfig.(*parsecli.HerokuAppConfig) 26 | if !ok { 27 | return stackerr.New("Invalid Heroku app config") 28 | } 29 | releaseName := r.ReleaseName 30 | if releaseName == "" { 31 | lastSecond, err := r.getLastSecondVersion(e, appConfig.HerokuAppID) 32 | if err != nil { 33 | return err 34 | } 35 | releaseName = fmt.Sprintf("%d", lastSecond) 36 | } 37 | release, err := e.HerokuAPIClient.ReleaseRollback(appConfig.HerokuAppID, releaseName) 38 | if err != nil { 39 | return stackerr.Wrap(err) 40 | } 41 | fmt.Fprintf( 42 | e.Out, 43 | `%s 44 | Current version: %d 45 | `, 46 | release.Description, 47 | release.Version, 48 | ) 49 | return nil 50 | } 51 | 52 | func NewRollbackCmd(e *parsecli.Env) *cobra.Command { 53 | r := rollbackCmd{} 54 | cmd := &cobra.Command{ 55 | Use: "rollback [app]", 56 | Short: "Rolls back the version for given app at Heroku", 57 | Long: "Rolls back the version for given app at Heroku.", 58 | Run: parsecli.RunWithClient(e, r.run), 59 | } 60 | cmd.Flags().StringVarP(&r.ReleaseName, "release", "r", r.ReleaseName, "The release to rollback to") 61 | return cmd 62 | } 63 | -------------------------------------------------------------------------------- /herokucmd/unzip.go: -------------------------------------------------------------------------------- 1 | package herokucmd 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/facebookgo/stackerr" 10 | ) 11 | 12 | func unzip(zfile string, base string, target string) error { 13 | r, err := zip.OpenReader(zfile) 14 | if err != nil { 15 | return stackerr.Wrap(err) 16 | } 17 | defer r.Close() 18 | 19 | for _, f := range r.Reader.File { 20 | z, err := f.Open() 21 | if err != nil { 22 | return stackerr.Wrap(err) 23 | } 24 | defer z.Close() 25 | 26 | name, err := filepath.Rel(base, f.Name) 27 | if err != nil { 28 | return stackerr.Wrap(err) 29 | } 30 | path := filepath.Join(target, name) 31 | if f.FileInfo().IsDir() { 32 | err := os.MkdirAll(path, f.Mode()) 33 | if err != nil { 34 | return stackerr.Wrap(err) 35 | } 36 | } else { 37 | w, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, f.Mode()) 38 | if err != nil { 39 | return stackerr.Wrap(err) 40 | } 41 | defer w.Close() 42 | 43 | _, err = io.Copy(w, z) 44 | if err != nil { 45 | return stackerr.Wrap(err) 46 | } 47 | } 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/ParsePlatform/parse-cli/parsecli" 8 | "github.com/facebookgo/stackerr" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | const ( 13 | applications = "applications" 14 | ) 15 | 16 | type listCmd struct{} 17 | 18 | func (l *listCmd) printListOfApps(e *parsecli.Env) error { 19 | config, err := parsecli.ConfigFromDir(e.Root) 20 | if err != nil { 21 | if os.IsNotExist(err) { 22 | // print nothing if we are not in a parse app directory 23 | return nil 24 | } 25 | return stackerr.Wrap(err) 26 | } 27 | config.PrettyPrintApps(e) 28 | fmt.Fprintln(e.Out, "") 29 | return nil 30 | } 31 | 32 | func (l *listCmd) run(e *parsecli.Env, args []string) error { 33 | l.printListOfApps(e) 34 | 35 | var apps parsecli.Apps 36 | if err := apps.Login.AuthUser(e, false); err != nil { 37 | return err 38 | } 39 | var appName string 40 | if len(args) > 0 { 41 | appName = args[0] 42 | } 43 | return apps.ShowApps(e, appName) 44 | } 45 | 46 | func NewListCmd(e *parsecli.Env) *cobra.Command { 47 | l := listCmd{} 48 | return &cobra.Command{ 49 | Use: "list [app]", 50 | Short: "Lists properties of given Parse app and Parse apps associated with given project", 51 | Long: `Lists Parse apps and aliases added to the current Cloud Code directory 52 | when executed inside the directory. 53 | Additionally, it prints the list of Parse apps associated with current Parse account. 54 | If an optional app name is provided, it prints all the keys for that app.`, 55 | Run: parsecli.RunWithArgs(e, l.run), 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /list_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/ParsePlatform/parse-cli/parsecli" 9 | "github.com/facebookgo/ensure" 10 | ) 11 | 12 | func newListCmdHarness(t testing.TB) (*parsecli.Harness, *listCmd) { 13 | h := parsecli.NewHarness(t) 14 | l := &listCmd{} 15 | return h, l 16 | } 17 | 18 | func TestPrintListOneAppNoDefaultKey(t *testing.T) { 19 | t.Parallel() 20 | conf := &parsecli.ParseConfig{Applications: map[string]*parsecli.ParseAppConfig{"first": {}}} 21 | h := parsecli.NewHarness(t) 22 | defer h.Stop() 23 | conf.PrettyPrintApps(h.Env) 24 | ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with Cloud Code in the current directory:\n first\n") 25 | } 26 | 27 | func TestPrintListOneAppWithDefaultKey(t *testing.T) { 28 | t.Parallel() 29 | conf := &parsecli.ParseConfig{Applications: map[string]*parsecli.ParseAppConfig{"first": {}}} 30 | conf.Applications[parsecli.DefaultKey] = &parsecli.ParseAppConfig{Link: "first"} 31 | 32 | h := parsecli.NewHarness(t) 33 | defer h.Stop() 34 | conf.PrettyPrintApps(h.Env) 35 | ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with Cloud Code in the current directory:\n* first\n") 36 | } 37 | 38 | func TestPrintListTwoAppsWithDefaultKey(t *testing.T) { 39 | t.Parallel() 40 | conf := &parsecli.ParseConfig{Applications: map[string]*parsecli.ParseAppConfig{"first": {}, "second": {}}} 41 | conf.Applications[parsecli.DefaultKey] = &parsecli.ParseAppConfig{Link: "first"} 42 | 43 | h := parsecli.NewHarness(t) 44 | defer h.Stop() 45 | conf.PrettyPrintApps(h.Env) 46 | ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with Cloud Code in the current directory:\n* first\n second\n") 47 | } 48 | 49 | func TestPrintListTwoAppsWithLinks(t *testing.T) { 50 | t.Parallel() 51 | conf := &parsecli.ParseConfig{Applications: map[string]*parsecli.ParseAppConfig{"first": {}, "second": {Link: "first"}}} 52 | conf.Applications[parsecli.DefaultKey] = &parsecli.ParseAppConfig{Link: "first"} 53 | h := parsecli.NewHarness(t) 54 | defer h.Stop() 55 | conf.PrettyPrintApps(h.Env) 56 | ensure.DeepEqual(t, h.Out.String(), "The following apps are associated with Cloud Code in the current directory:\n* first\n second -> first\n") 57 | } 58 | func TestPrintListNoConfig(t *testing.T) { 59 | t.Parallel() 60 | h, l := newListCmdHarness(t) 61 | h.MakeEmptyRoot() 62 | defer h.Stop() 63 | ensure.NotNil(t, l.printListOfApps(h.Env)) 64 | ensure.DeepEqual(t, h.Out.String(), "") 65 | } 66 | 67 | func TestPrintListNoApps(t *testing.T) { 68 | t.Parallel() 69 | h, l := newListCmdHarness(t) 70 | h.MakeEmptyRoot() 71 | defer h.Stop() 72 | parsecli.CloneSampleCloudCode(h.Env, true) 73 | ensure.Nil(t, l.printListOfApps(h.Env)) 74 | } 75 | 76 | func TestRunListCmd(t *testing.T) { 77 | t.Parallel() 78 | 79 | h, _ := parsecli.NewAppHarness(t) 80 | defer h.Stop() 81 | 82 | l := &listCmd{} 83 | h.Env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) 84 | ensure.Nil(t, l.run(h.Env, []string{"A"})) 85 | ensure.StringContains(t, h.Out.String(), `Properties of the app "A"`) 86 | } 87 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "os/signal" 9 | "strings" 10 | "time" 11 | 12 | "github.com/ParsePlatform/parse-cli/parsecli" 13 | "github.com/facebookgo/clock" 14 | "github.com/facebookgo/stackerr" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | func main() { 19 | // some parts of apps.go are unable to handle 20 | // interrupts, this logic ensures we exit on system interrupts 21 | interrupt := make(chan os.Signal, 1) 22 | signal.Notify(interrupt, os.Interrupt) 23 | go func() { 24 | <-interrupt 25 | os.Exit(1) 26 | }() 27 | 28 | e := parsecli.Env{ 29 | Root: os.Getenv("PARSE_ROOT"), 30 | Server: os.Getenv("PARSE_SERVER"), 31 | ErrorStack: os.Getenv("PARSE_ERROR_STACK") == "1", 32 | ParserEmail: os.Getenv("PARSER_EMAIL"), 33 | Out: os.Stdout, 34 | Err: os.Stderr, 35 | In: os.Stdin, 36 | Exit: os.Exit, 37 | Clock: clock.New(), 38 | } 39 | if e.Root == "" { 40 | cur, err := os.Getwd() 41 | if err != nil { 42 | fmt.Fprintf(e.Err, "Failed to get current directory:\n%s\n", err) 43 | os.Exit(1) 44 | } 45 | root := parsecli.GetProjectRoot(&e, cur) 46 | if parsecli.IsProjectDir(root) { 47 | e.Root = root 48 | config, err := parsecli.ConfigFromDir(root) 49 | if err != nil { 50 | fmt.Fprintln(e.Err, err) 51 | os.Exit(1) 52 | } 53 | e.Type = config.GetProjectConfig().Type 54 | if e.ParserEmail == "" { 55 | e.ParserEmail = config.GetProjectConfig().ParserEmail 56 | } 57 | } else { 58 | e.Type = parsecli.LegacyParseFormat 59 | e.Root = parsecli.GetLegacyProjectRoot(&e, cur) 60 | } 61 | } 62 | if e.Type != parsecli.LegacyParseFormat && e.Type != parsecli.ParseFormat && e.Type != parsecli.HerokuFormat { 63 | fmt.Fprintf(e.Err, "Unknown project type %d.\n", e.Type) 64 | os.Exit(1) 65 | } 66 | 67 | if e.Server == "" { 68 | e.Server = parsecli.DefaultBaseURL 69 | } 70 | 71 | apiClient, err := parsecli.NewParseAPIClient(&e) 72 | if err != nil { 73 | fmt.Fprintln(e.Err, err) 74 | os.Exit(1) 75 | } 76 | e.ParseAPIClient = apiClient 77 | if e.Type == parsecli.HerokuFormat { 78 | apiClient, err := parsecli.NewHerokuAPIClient(&e) 79 | if err != nil { 80 | fmt.Fprintln(e.Err, err) 81 | os.Exit(1) 82 | } 83 | e.HerokuAPIClient = apiClient 84 | } 85 | 86 | var ( 87 | rootCmd *cobra.Command 88 | command []string 89 | ) 90 | var mode string 91 | switch e.Type { 92 | case parsecli.LegacyParseFormat, parsecli.ParseFormat: 93 | mode = "parse" 94 | command, rootCmd = parseRootCmd(&e) 95 | case parsecli.HerokuFormat: 96 | mode = "heroku" 97 | command, rootCmd = herokuRootCmd(&e) 98 | } 99 | 100 | if len(command) == 0 || command[0] != "update" { 101 | message, err := checkIfSupported(&e, parsecli.Version, mode, command...) 102 | if err != nil { 103 | fmt.Fprintln(e.Err, err) 104 | os.Exit(1) 105 | } 106 | if message != "" { 107 | fmt.Fprintln(e.Err, message) 108 | } 109 | } 110 | 111 | if err := rootCmd.Execute(); err != nil { 112 | // Error is already printed in Execute() 113 | os.Exit(1) 114 | } 115 | } 116 | 117 | func checkIfSupported(e *parsecli.Env, version string, mode string, args ...string) (string, error) { 118 | v := make(url.Values) 119 | v.Set("version", version) 120 | v.Set("mode", mode) 121 | v.Set("other", strings.Join(args, " ")) 122 | req := &http.Request{ 123 | Method: "GET", 124 | URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, 125 | } 126 | 127 | type result struct { 128 | warning string 129 | err error 130 | } 131 | 132 | timeout := make(chan *result, 1) 133 | go func() { 134 | var res struct { 135 | Warning string `json:"warning"` 136 | } 137 | _, err := e.ParseAPIClient.Do(req, nil, &res) 138 | timeout <- &result{warning: res.Warning, err: err} 139 | }() 140 | 141 | select { 142 | case res := <-timeout: 143 | return res.warning, stackerr.Wrap(res.err) 144 | case <-time.After(time.Second): 145 | return "", nil 146 | } 147 | return "", nil 148 | } 149 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/ParsePlatform/parse-cli/parsecli" 10 | "github.com/facebookgo/ensure" 11 | "github.com/facebookgo/jsonpipe" 12 | "github.com/facebookgo/parse" 13 | ) 14 | 15 | func TestNewClientInvalidServerURL(t *testing.T) { 16 | t.Parallel() 17 | c, err := parsecli.NewParseAPIClient(&parsecli.Env{Server: ":"}) 18 | ensure.True(t, c == nil) 19 | ensure.Err(t, err, regexp.MustCompile("invalid server URL")) 20 | } 21 | 22 | func TestIsSupportedWarning(t *testing.T) { 23 | t.Parallel() 24 | 25 | h := parsecli.NewHarness(t) 26 | defer h.Stop() 27 | 28 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 29 | ensure.DeepEqual(t, r.URL.Path, "/1/supported") 30 | return &http.Response{ 31 | StatusCode: http.StatusOK, 32 | Body: ioutil.NopCloser( 33 | jsonpipe.Encode( 34 | map[string]string{"warning": "please update"}, 35 | ), 36 | ), 37 | }, nil 38 | }) 39 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 40 | message, err := checkIfSupported(h.Env, "2.0.2", "parse") 41 | ensure.Nil(t, err) 42 | ensure.DeepEqual(t, message, "please update") 43 | } 44 | 45 | func TestIsSupportedError(t *testing.T) { 46 | t.Parallel() 47 | 48 | h := parsecli.NewHarness(t) 49 | defer h.Stop() 50 | 51 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 52 | ensure.DeepEqual(t, r.URL.Path, "/1/supported") 53 | return &http.Response{ 54 | StatusCode: http.StatusBadRequest, 55 | Body: ioutil.NopCloser( 56 | jsonpipe.Encode( 57 | map[string]string{"error": "not supported"}, 58 | ), 59 | ), 60 | }, nil 61 | }) 62 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 63 | _, err := checkIfSupported(h.Env, "2.0.2", "parse") 64 | ensure.Err(t, err, regexp.MustCompile("not supported")) 65 | } 66 | 67 | // this just exists to make code coverage less noisy. 68 | func TestRootCommand(t *testing.T) { 69 | t.Parallel() 70 | h := parsecli.NewHarness(t) 71 | defer h.Stop() 72 | parseRootCmd(h.Env) 73 | } 74 | -------------------------------------------------------------------------------- /migrate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/ParsePlatform/parse-cli/parsecli" 9 | "github.com/facebookgo/stackerr" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type migrateCmd struct { 14 | retainMaster bool 15 | } 16 | 17 | func (m *migrateCmd) upgradeLegacy(e *parsecli.Env, c parsecli.Config) (*parsecli.ParseConfig, error) { 18 | p := c.GetProjectConfig() 19 | if p.Type != parsecli.LegacyParseFormat { 20 | return nil, stackerr.New("Already using the preferred config format.") 21 | } 22 | p.Type = parsecli.ParseFormat 23 | config, ok := (c).(*parsecli.ParseConfig) 24 | if !ok { 25 | return nil, stackerr.Newf("Unexpected config format: %d", p.Type) 26 | } 27 | if !m.retainMaster { 28 | for _, app := range config.Applications { 29 | app.MasterKey = "" 30 | } 31 | } 32 | return config, nil 33 | } 34 | 35 | func (m *migrateCmd) run(e *parsecli.Env) error { 36 | c, err := parsecli.ConfigFromDir(e.Root) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | c, err = m.upgradeLegacy(e, c) 42 | if err != nil { 43 | return err 44 | } 45 | localErr := parsecli.StoreConfig(e, c) 46 | projectErr := parsecli.StoreProjectConfig(e, c) 47 | if localErr == nil && projectErr == nil { 48 | legacy := filepath.Join(e.Root, parsecli.LegacyConfigFile) 49 | err := os.Remove(legacy) 50 | if err != nil { 51 | fmt.Fprintf(e.Err, "Could not delete: %q. Please remove this file manually.\n", legacy) 52 | } 53 | } else { 54 | local := filepath.Join(e.Root, parsecli.ParseLocal) 55 | err := os.Remove(local) 56 | if err != nil { 57 | fmt.Fprintf(e.Err, "Failed to clean up: %q. Please remove this file manually.\n", local) 58 | } 59 | project := filepath.Join(e.Root, parsecli.ParseProject) 60 | err = os.Remove(project) 61 | if err != nil { 62 | fmt.Fprintf(e.Err, "Failed to clean up: %q. Please remove this file manually.\n", project) 63 | } 64 | } 65 | fmt.Fprintln(e.Out, "Successfully migrated to the preferred config format.") 66 | return nil 67 | } 68 | 69 | func NewMigrateCmd(e *parsecli.Env) *cobra.Command { 70 | var m migrateCmd 71 | cmd := &cobra.Command{ 72 | Use: "migrate", 73 | Short: "Migrate project config format to preferred format", 74 | Long: `Use this on projects with legacy config format to migrate 75 | to the preferred format. 76 | `, 77 | Run: parsecli.RunNoArgs(e, m.run), 78 | } 79 | cmd.Flags().BoolVarP(&m.retainMaster, "retain", "r", m.retainMaster, 80 | "Retain any master keys present in config during migration") 81 | return cmd 82 | } 83 | -------------------------------------------------------------------------------- /migrate_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/ParsePlatform/parse-cli/parsecli" 12 | "github.com/facebookgo/ensure" 13 | ) 14 | 15 | func TestUpgradeLegacyNoOp(t *testing.T) { 16 | t.Parallel() 17 | 18 | h := parsecli.NewHarness(t) 19 | defer h.Stop() 20 | 21 | var m migrateCmd 22 | c := &parsecli.ParseConfig{ProjectConfig: &parsecli.ProjectConfig{Type: parsecli.ParseFormat}} 23 | _, err := m.upgradeLegacy(h.Env, c) 24 | ensure.Err(t, err, regexp.MustCompile("Already using the preferred config format.")) 25 | } 26 | 27 | func TestUpgradeLegacyRetainMaster(t *testing.T) { 28 | t.Parallel() 29 | 30 | h := parsecli.NewHarness(t) 31 | defer h.Stop() 32 | 33 | m := migrateCmd{retainMaster: true} 34 | c := &parsecli.ParseConfig{ 35 | Applications: map[string]*parsecli.ParseAppConfig{ 36 | "app": {ApplicationID: "a", MasterKey: "m"}, 37 | }, 38 | ProjectConfig: &parsecli.ProjectConfig{Type: parsecli.LegacyParseFormat}, 39 | } 40 | config, err := m.upgradeLegacy(h.Env, c) 41 | ensure.Nil(t, err) 42 | ensure.DeepEqual(t, config.Applications["app"].MasterKey, "m") 43 | } 44 | 45 | func TestUpgradeLegacy(t *testing.T) { 46 | t.Parallel() 47 | 48 | h := parsecli.NewHarness(t) 49 | defer h.Stop() 50 | 51 | m := migrateCmd{retainMaster: false} 52 | c := &parsecli.ParseConfig{ 53 | Applications: map[string]*parsecli.ParseAppConfig{ 54 | "app": {ApplicationID: "a", MasterKey: "m"}, 55 | }, 56 | ProjectConfig: &parsecli.ProjectConfig{Type: parsecli.LegacyParseFormat}, 57 | } 58 | config, err := m.upgradeLegacy(h.Env, c) 59 | ensure.Nil(t, err) 60 | ensure.DeepEqual(t, config.Applications["app"].MasterKey, "") 61 | } 62 | 63 | func TestUpgradeLegacyWithEmail(t *testing.T) { 64 | t.Parallel() 65 | 66 | h := parsecli.NewHarness(t) 67 | defer h.Stop() 68 | 69 | m := migrateCmd{retainMaster: false} 70 | c := &parsecli.ParseConfig{ 71 | Applications: map[string]*parsecli.ParseAppConfig{ 72 | "app": {ApplicationID: "a", MasterKey: "m"}, 73 | }, 74 | ProjectConfig: &parsecli.ProjectConfig{ 75 | Type: parsecli.LegacyParseFormat, 76 | ParserEmail: "test@email.com", 77 | }, 78 | } 79 | config, err := m.upgradeLegacy(h.Env, c) 80 | ensure.Nil(t, err) 81 | ensure.DeepEqual(t, config.Applications["app"].MasterKey, "") 82 | ensure.DeepEqual(t, config.GetProjectConfig().ParserEmail, "test@email.com") 83 | } 84 | 85 | func TestRunMigrateCmd(t *testing.T) { 86 | t.Parallel() 87 | 88 | h := parsecli.NewHarness(t) 89 | h.MakeEmptyRoot() 90 | defer h.Stop() 91 | 92 | n := &newCmd{} 93 | h.Env.Type = parsecli.ParseFormat 94 | h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) 95 | _, err := n.setupSample(h.Env, 96 | "yolo", 97 | &parsecli.ParseAppConfig{ 98 | ApplicationID: "yolo-id", 99 | MasterKey: "yoda", 100 | }, 101 | false, 102 | false, 103 | ) 104 | ensure.Nil(t, err) 105 | 106 | m := &migrateCmd{} 107 | err = m.run(h.Env) 108 | ensure.Err(t, err, regexp.MustCompile("Already using the preferred config format")) 109 | 110 | ensure.Nil(t, os.Remove(filepath.Join(h.Env.Root, parsecli.ParseLocal))) 111 | ensure.Nil(t, os.Remove(filepath.Join(h.Env.Root, parsecli.ParseProject))) 112 | 113 | ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) 114 | ensure.Nil(t, 115 | ioutil.WriteFile( 116 | filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), 117 | []byte(`{ 118 | "applications": { 119 | "yolo": { 120 | "applicationId": "yolo-id", 121 | "masterKey": "yoda" 122 | } 123 | } 124 | }`), 125 | 0600, 126 | ), 127 | ) 128 | 129 | ensure.Nil(t, m.run(h.Env)) 130 | ensure.StringContains(t, h.Out.String(), "Successfully migrated") 131 | } 132 | -------------------------------------------------------------------------------- /parse_commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/ParsePlatform/parse-cli/parsecli" 9 | "github.com/ParsePlatform/parse-cli/parsecmd" 10 | "github.com/ParsePlatform/parse-cli/webhooks" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func parseRootCmd(e *parsecli.Env) ([]string, *cobra.Command) { 15 | c := &cobra.Command{ 16 | Use: "parse", 17 | Long: fmt.Sprintf( 18 | `Parse Command Line Interface 19 | Version %v 20 | Copyright %d Parse, Inc. 21 | http://parse.com`, 22 | parsecli.Version, 23 | time.Now().Year(), 24 | ), 25 | Run: func(cmd *cobra.Command, args []string) { 26 | cmd.Help() 27 | }, 28 | } 29 | 30 | c.AddCommand(NewAddCmd(e)) 31 | c.AddCommand(NewConfigureCmd(e)) 32 | c.AddCommand(NewDefaultCmd(e)) 33 | c.AddCommand(parsecmd.NewDeployCmd(e)) 34 | c.AddCommand(parsecmd.NewDevelopCmd(e)) 35 | c.AddCommand(parsecmd.NewDownloadCmd(e)) 36 | c.AddCommand(webhooks.NewFunctionHooksCmd(e)) 37 | c.AddCommand(parsecmd.NewGenerateCmd(e)) 38 | c.AddCommand(parsecmd.NewJsSdkCmd(e)) 39 | c.AddCommand(NewListCmd(e)) 40 | c.AddCommand(parsecmd.NewLogsCmd(e)) 41 | c.AddCommand(NewMigrateCmd(e)) 42 | c.AddCommand(NewNewCmd(e)) 43 | c.AddCommand(parsecmd.NewReleasesCmd(e)) 44 | c.AddCommand(parsecmd.NewRollbackCmd(e)) 45 | c.AddCommand(parsecmd.NewSymbolsCmd(e)) 46 | c.AddCommand(webhooks.NewTriggerHooksCmd(e)) 47 | c.AddCommand(NewUpdateCmd(e)) 48 | c.AddCommand(NewVersionCmd(e)) 49 | 50 | if len(os.Args) <= 1 { 51 | return nil, c 52 | } 53 | 54 | commands := []string{"help"} 55 | for _, command := range c.Commands() { 56 | commands = append(commands, command.Name()) 57 | } 58 | 59 | args := make([]string, len(os.Args)-1) 60 | copy(args, os.Args[1:]) 61 | 62 | if message := parsecli.MakeCorrections(commands, args); message != "" { 63 | fmt.Fprintln(e.Out, message) 64 | } 65 | c.SetArgs(args) 66 | 67 | return args, c 68 | } 69 | -------------------------------------------------------------------------------- /parsecli/apps_test.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net/http" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/facebookgo/ensure" 12 | "github.com/facebookgo/parse" 13 | ) 14 | 15 | var ( 16 | defaultCredentials = Credentials{Email: "email", Password: "password"} 17 | defaultTokenCredentials = Credentials{Email: "email", Token: "token"} 18 | defaultApps = Apps{Login: Login{Credentials: defaultCredentials}} 19 | defaultAppsWithToken = Apps{Login: Login{Credentials: defaultTokenCredentials}} 20 | ) 21 | 22 | func TestFetchAppKeys(t *testing.T) { 23 | t.Parallel() 24 | 25 | h, _ := NewAppHarness(t) 26 | defer h.Stop() 27 | 28 | h.Env.In = ioutil.NopCloser(strings.NewReader("email\npassword\n")) 29 | app, err := FetchAppKeys(h.Env, "an-app") 30 | ensure.Nil(t, err) 31 | ensure.DeepEqual(t, app, newTestApp("an-app")) 32 | } 33 | 34 | func TestFetchApps(t *testing.T) { 35 | t.Parallel() 36 | 37 | h, apps := NewAppHarness(t) 38 | defer h.Stop() 39 | 40 | a := defaultApps 41 | gotApps, err := a.RestFetchApps(h.Env) 42 | ensure.Nil(t, err) 43 | ensure.DeepEqual(t, gotApps, apps) 44 | 45 | a.Login.Credentials.Password = "invalid" 46 | _, err = a.RestFetchApps(h.Env) 47 | ensure.DeepEqual(t, err, errAuth) 48 | } 49 | 50 | func TestFetchAppsToken(t *testing.T) { 51 | t.Parallel() 52 | 53 | h, apps := NewAppHarness(t) 54 | defer h.Stop() 55 | 56 | a := defaultAppsWithToken 57 | gotApps, err := a.RestFetchApps(h.Env) 58 | ensure.Nil(t, err) 59 | ensure.DeepEqual(t, gotApps, apps) 60 | 61 | a.Login.Credentials.Token = "invalid" 62 | _, err = a.RestFetchApps(h.Env) 63 | ensure.DeepEqual(t, err, errAuth) 64 | } 65 | 66 | func TestSelectAppWithRetries(t *testing.T) { 67 | t.Parallel() 68 | 69 | h, apps := NewAppHarness(t) 70 | 71 | defer h.Stop() 72 | 73 | a := defaultApps 74 | h.Env.In = ioutil.NopCloser(strings.NewReader("3\n1")) 75 | selectedApp, err := a.SelectApp(apps, "test: ", h.Env) 76 | ensure.Nil(t, err) 77 | ensure.DeepEqual(t, h.Out.String(), 78 | `1: A 79 | 2: B 80 | test: Invalid selection 3: must be between 1 and 2 81 | 1: A 82 | 2: B 83 | test: `) 84 | ensure.DeepEqual(t, selectedApp, newTestApp("A")) 85 | } 86 | 87 | func TestSelectApp(t *testing.T) { 88 | t.Parallel() 89 | 90 | // apps = [B, A] & appNames=[A,B] 91 | h, apps := NewAppHarness(t) 92 | apps[0], apps[1] = apps[1], apps[0] 93 | 94 | defer h.Stop() 95 | 96 | a := defaultApps 97 | h.Env.In = ioutil.NopCloser(strings.NewReader("1")) 98 | selectedApp, err := a.SelectApp(apps, "test: ", h.Env) 99 | ensure.Nil(t, err) 100 | ensure.DeepEqual(t, h.Out.String(), 101 | `1: A 102 | 2: B 103 | test: `) 104 | ensure.DeepEqual(t, selectedApp, newTestApp("A")) 105 | } 106 | 107 | func TestGetAppName(t *testing.T) { 108 | t.Parallel() 109 | 110 | h := NewHarness(t) 111 | defer h.Stop() 112 | 113 | a := defaultApps 114 | h.Env.In = ioutil.NopCloser(strings.NewReader("\n")) 115 | appName, err := a.getAppName(h.Env) 116 | ensure.Err(t, err, regexp.MustCompile("App name cannot be empty")) 117 | 118 | h.Env.In = ioutil.NopCloser(strings.NewReader("hello\n")) 119 | appName, err = a.getAppName(h.Env) 120 | ensure.Nil(t, err) 121 | ensure.DeepEqual(t, appName, "hello") 122 | } 123 | 124 | func TestCreateApp(t *testing.T) { 125 | t.Parallel() 126 | 127 | h, _ := NewAppHarness(t) 128 | defer h.Stop() 129 | 130 | a := defaultApps 131 | app, err := a.restCreateApp(h.Env, "C") 132 | 133 | ensure.Nil(t, err) 134 | ensure.DeepEqual(t, app, newTestApp("C")) 135 | } 136 | 137 | func TestCreateNewApp(t *testing.T) { 138 | t.Parallel() 139 | 140 | h, _ := NewAppHarness(t) 141 | defer h.Stop() 142 | 143 | a := defaultApps 144 | h.Env.In = ioutil.NopCloser(strings.NewReader("D")) 145 | app, err := a.CreateApp(h.Env, "", 0) 146 | ensure.Nil(t, err) 147 | 148 | ensure.Nil(t, a.PrintApp(h.Env, app)) 149 | ensure.DeepEqual(t, h.Out.String(), 150 | `Please choose a name for your Parse app. 151 | Note that this name will appear on the Parse website, 152 | but it does not have to be the same as your mobile app's public name. 153 | Name: Properties of the app "D": 154 | Name D 155 | DashboardURL https://api.example.com/dashboard/D 156 | ApplicationID applicationID.D 157 | ClientKey clientKey.D 158 | JavaScriptKey javaScriptKey.D 159 | WindowsKey windowsKey.D 160 | WebhookKey webhookKey.D 161 | RestKey restKey.D 162 | MasterKey masterKey.D 163 | ClientPushEnabled false 164 | ClientClassCreationEnabled true 165 | RequireRevocableSessions true 166 | RevokeSessionOnPasswordChange true 167 | `) 168 | } 169 | 170 | func TestCreateNewAppNameTaken(t *testing.T) { 171 | t.Parallel() 172 | 173 | h, _ := NewAppHarness(t) 174 | defer h.Stop() 175 | 176 | a := defaultApps 177 | 178 | h.Env.In = ioutil.NopCloser(strings.NewReader("A\nD")) 179 | _, err := a.CreateApp(h.Env, "", 0) 180 | ensure.Nil(t, err) 181 | ensure.StringContains(t, h.Err.String(), "already created an app") 182 | } 183 | 184 | func TestNoPanicAppCreate(t *testing.T) { 185 | t.Parallel() 186 | h := NewHarness(t) 187 | defer h.Stop() 188 | 189 | ht := TransportFunc(func(r *http.Request) (*http.Response, error) { 190 | ensure.DeepEqual(t, r.URL.Path, "/1/apps") 191 | return nil, errors.New("nil response panic") 192 | }) 193 | h.Env.ParseAPIClient = &ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 194 | 195 | var apps Apps 196 | app, err := apps.restCreateApp(h.Env, "panic!") 197 | ensure.True(t, app == nil) 198 | ensure.Err(t, err, regexp.MustCompile("nil response panic")) 199 | } 200 | -------------------------------------------------------------------------------- /parsecli/auto_correct.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/xrash/smetrics" 10 | ) 11 | 12 | const ( 13 | levenThreshold = 4 14 | jaroThreshold = 0.70 15 | ) 16 | 17 | type cmdScore struct { 18 | name string 19 | levenshtein int 20 | jaroWinkler float64 21 | } 22 | 23 | type cmdScores []cmdScore 24 | 25 | func (s cmdScores) Len() int { 26 | return len(s) 27 | } 28 | 29 | func (s cmdScores) Less(i, j int) bool { 30 | if s[i].levenshtein < s[j].levenshtein { 31 | return true 32 | } 33 | if s[i].levenshtein == s[j].levenshtein { 34 | return s[i].jaroWinkler > s[j].jaroWinkler 35 | } 36 | return false 37 | } 38 | 39 | func (s cmdScores) Swap(i, j int) { 40 | s[i], s[j] = s[j], s[i] 41 | } 42 | 43 | func getArgPos(args []string) int { 44 | pos := -1 45 | for i, arg := range args { 46 | if !strings.HasPrefix(arg, "-") { 47 | pos = i 48 | break 49 | } 50 | } 51 | return pos 52 | } 53 | 54 | // SuggestCommands can be used to match a given word 55 | // which is very similar to one among the list of available commands. 56 | func SuggestCommands(given string, available []string) []string { 57 | commandSet := make(map[string]struct{}) 58 | for _, command := range available { 59 | commandSet[command] = struct{}{} 60 | } 61 | if len(commandSet) == 0 || given == "" { 62 | return nil 63 | } 64 | 65 | if _, ok := commandSet[given]; ok { 66 | return nil 67 | } 68 | 69 | var scores cmdScores 70 | 71 | for command := range commandSet { 72 | levenshtein := smetrics.WagnerFischer(command, given, 1, 1, 1) 73 | jaroWinkler := smetrics.JaroWinkler(command, given, 0.7, 4) 74 | 75 | if levenshtein > levenThreshold { 76 | continue 77 | } 78 | if jaroWinkler < jaroThreshold { 79 | continue 80 | } 81 | scores = append( 82 | scores, 83 | cmdScore{ 84 | name: command, 85 | levenshtein: levenshtein, 86 | jaroWinkler: jaroWinkler, 87 | }, 88 | ) 89 | } 90 | if len(scores) == 0 { 91 | return nil 92 | } 93 | sort.Sort(scores) 94 | 95 | levenshtein := scores[0].levenshtein 96 | 97 | var matches []string 98 | for _, score := range scores { 99 | if score.levenshtein != levenshtein { 100 | break 101 | } 102 | matches = append(matches, score.name) 103 | } 104 | 105 | return matches 106 | } 107 | 108 | // MakeCorrections can be used to automatically correct the incorrect command in input args 109 | // and return either an action that was taken or a suggestions message. 110 | // One can also provide a map of aliases and related conversions 111 | func MakeCorrections(commands []string, args []string) string { 112 | pos := getArgPos(args) 113 | if pos == -1 { 114 | return "" 115 | } 116 | 117 | arg := args[pos] 118 | 119 | matches := SuggestCommands(arg, commands) 120 | if len(matches) == 0 { 121 | return "" 122 | } 123 | if len(matches) == 1 { 124 | args[pos] = matches[0] 125 | return fmt.Sprintf("(assuming by `%s` you meant `%s`)", arg, matches[0]) 126 | } 127 | 128 | var buffer bytes.Buffer 129 | for _, match := range matches { 130 | buffer.WriteString("+ ") 131 | buffer.WriteString(match) 132 | buffer.WriteRune('\n') 133 | } 134 | return fmt.Sprintf("ambiguous subcommand:\tdid you mean one of:\n%s", buffer.String()) 135 | } 136 | -------------------------------------------------------------------------------- /parsecli/auto_correct_test.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/facebookgo/ensure" 7 | ) 8 | 9 | func TestCmdScoreLess(t *testing.T) { 10 | t.Parallel() 11 | scores := cmdScores([]cmdScore{ 12 | {levenshtein: 1, jaroWinkler: 0.9}, 13 | {levenshtein: 2, jaroWinkler: 0.8}, 14 | }) 15 | 16 | ensure.True(t, scores.Less(0, 1)) 17 | 18 | scores = cmdScores([]cmdScore{ 19 | {levenshtein: 1, jaroWinkler: 0.9}, 20 | {levenshtein: 1, jaroWinkler: 0.8}, 21 | }) 22 | 23 | ensure.True(t, scores.Less(0, 1)) 24 | 25 | scores = cmdScores([]cmdScore{ 26 | {levenshtein: 1, jaroWinkler: 0.8}, 27 | {levenshtein: 1, jaroWinkler: 0.9}, 28 | }) 29 | 30 | ensure.False(t, scores.Less(0, 1)) 31 | 32 | scores = cmdScores([]cmdScore{ 33 | {levenshtein: 2, jaroWinkler: 0.9}, 34 | {levenshtein: 1, jaroWinkler: 0.8}, 35 | }) 36 | 37 | ensure.False(t, scores.Less(0, 1)) 38 | } 39 | 40 | func TestMakeCorrections(t *testing.T) { 41 | t.Parallel() 42 | 43 | ensure.DeepEqual(t, MakeCorrections(nil, nil), "") 44 | ensure.DeepEqual(t, MakeCorrections(nil, []string{"arg"}), "") 45 | 46 | subCommands := []string{"cmd"} 47 | ensure.DeepEqual(t, MakeCorrections(subCommands, nil), "") 48 | 49 | ensure.DeepEqual(t, MakeCorrections(subCommands, []string{"--file"}), "") 50 | ensure.DeepEqual(t, MakeCorrections(subCommands, []string{"-f"}), "") 51 | ensure.DeepEqual(t, MakeCorrections(subCommands, []string{"--file", "-p"}), "") 52 | ensure.DeepEqual(t, MakeCorrections(subCommands, []string{"-p", "--file"}), "") 53 | 54 | args := []string{"--flags", "cmd", "args"} 55 | ensure.DeepEqual(t, MakeCorrections(subCommands, args), "") 56 | ensure.DeepEqual(t, args, []string{"--flags", "cmd", "args"}) 57 | } 58 | 59 | func TestMatchesForSubCommands(t *testing.T) { 60 | t.Parallel() 61 | 62 | subCommands := []string{"version", "deploy", "deplore"} 63 | 64 | testCases := []struct { 65 | args []string 66 | modArgs []string 67 | message string 68 | }{ 69 | { 70 | []string{"version"}, 71 | []string{"version"}, 72 | "", 73 | }, 74 | { 75 | []string{"vers"}, 76 | []string{"version"}, 77 | "(assuming by `vers` you meant `version`)", 78 | }, 79 | { 80 | []string{"depluy", "-f", "-v"}, 81 | []string{"deploy", "-f", "-v"}, 82 | "(assuming by `depluy` you meant `deploy`)", 83 | }, 84 | { 85 | []string{"-v", "deple", "-f"}, 86 | []string{"-v", "deple", "-f"}, 87 | `ambiguous subcommand: did you mean one of: 88 | + deploy 89 | + deplore 90 | `, 91 | }, 92 | } 93 | 94 | for _, testCase := range testCases { 95 | ensure.DeepEqual(t, MakeCorrections(subCommands, testCase.args), testCase.message) 96 | ensure.DeepEqual(t, testCase.args, testCase.modArgs) 97 | } 98 | 99 | errorCases := []struct { 100 | args []string 101 | modeArgs []string 102 | }{ 103 | { 104 | []string{"clivers"}, 105 | []string{"version"}, 106 | }, 107 | { 108 | []string{"dupiay"}, 109 | []string{"deploy"}, 110 | }, 111 | } 112 | for _, errorCase := range errorCases { 113 | ensure.DeepEqual(t, MakeCorrections(subCommands, errorCase.args), "") 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /parsecli/client.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | 7 | "github.com/bgentry/heroku-go" 8 | "github.com/facebookgo/parse" 9 | "github.com/facebookgo/stackerr" 10 | ) 11 | 12 | // ParseAPIClient is the http client used by parse-cli 13 | type ParseAPIClient struct { 14 | APIClient *parse.Client 15 | } 16 | 17 | func NewParseAPIClient(e *Env) (*ParseAPIClient, error) { 18 | baseURL, err := url.Parse(e.Server) 19 | if err != nil { 20 | return nil, stackerr.Newf("invalid server URL %q: %s", e.Server, err) 21 | } 22 | return &ParseAPIClient{ 23 | APIClient: &parse.Client{ 24 | BaseURL: baseURL, 25 | }, 26 | }, nil 27 | } 28 | 29 | func NewHerokuAPIClient(e *Env) (*heroku.Client, error) { 30 | return &heroku.Client{}, nil 31 | } 32 | 33 | func (c *ParseAPIClient) appendCommonHeaders(header http.Header) http.Header { 34 | if header == nil { 35 | header = make(http.Header) 36 | } 37 | header.Add("User-Agent", UserAgent) 38 | return header 39 | } 40 | 41 | // Get performs a GET method call on the given url and unmarshal response into 42 | // result. 43 | func (c *ParseAPIClient) Get(u *url.URL, result interface{}) (*http.Response, error) { 44 | return c.Do(&http.Request{Method: "GET", URL: u}, nil, result) 45 | } 46 | 47 | // Post performs a POST method call on the given url with the given body and 48 | // unmarshal response into result. 49 | func (c *ParseAPIClient) Post(u *url.URL, body, result interface{}) (*http.Response, error) { 50 | return c.Do(&http.Request{Method: "POST", URL: u}, body, result) 51 | } 52 | 53 | // Put performs a PUT method call on the given url with the given body and 54 | // unmarshal response into result. 55 | func (c *ParseAPIClient) Put(u *url.URL, body, result interface{}) (*http.Response, error) { 56 | return c.Do(&http.Request{Method: "PUT", URL: u}, body, result) 57 | } 58 | 59 | // Delete performs a DELETE method call on the given url and unmarshal response 60 | // into result. 61 | func (c *ParseAPIClient) Delete(u *url.URL, result interface{}) (*http.Response, error) { 62 | return c.Do(&http.Request{Method: "DELETE", URL: u}, nil, result) 63 | } 64 | 65 | // RoundTrip is a wrapper for parse.Client.RoundTrip 66 | func (c *ParseAPIClient) RoundTrip(req *http.Request) (*http.Response, error) { 67 | req.Header = c.appendCommonHeaders(req.Header) 68 | return c.APIClient.RoundTrip(req) 69 | } 70 | 71 | // Do is a wrapper for parse.Client.Do 72 | func (c *ParseAPIClient) Do(req *http.Request, body, result interface{}) (*http.Response, error) { 73 | req.Header = c.appendCommonHeaders(req.Header) 74 | return c.APIClient.Do(req, body, result) 75 | } 76 | 77 | // WithCredentials is a wrapper for parse.Client.WithCredentials 78 | func (c *ParseAPIClient) WithCredentials(cr parse.Credentials) *ParseAPIClient { 79 | c.APIClient = c.APIClient.WithCredentials(cr) 80 | return c 81 | } 82 | -------------------------------------------------------------------------------- /parsecli/config_test.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/facebookgo/ensure" 10 | ) 11 | 12 | func TestGetConfigFile(t *testing.T) { 13 | t.Parallel() 14 | h := NewHarness(t) 15 | defer h.Stop() 16 | 17 | h.Env.Type = LegacyParseFormat 18 | ensure.DeepEqual(t, GetConfigFile(h.Env), filepath.Join(h.Env.Root, LegacyConfigFile)) 19 | 20 | h.Env.Type = ParseFormat 21 | ensure.DeepEqual(t, GetConfigFile(h.Env), filepath.Join(h.Env.Root, ParseLocal)) 22 | } 23 | 24 | func TestConfigFromDirFormatType(t *testing.T) { 25 | t.Parallel() 26 | h := NewHarness(t) 27 | h.MakeEmptyRoot() 28 | defer h.Stop() 29 | 30 | ensure.Nil(t, CloneSampleCloudCode(h.Env, true)) 31 | 32 | c, err := ConfigFromDir(h.Env.Root) 33 | ensure.Nil(t, err) 34 | 35 | ensure.DeepEqual(t, c.GetProjectConfig().Type, ParseFormat) 36 | 37 | ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, ConfigDir), 0755)) 38 | ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.Env.Root, LegacyConfigFile), 39 | []byte("{}"), 40 | 0600), 41 | ) 42 | c, err = ConfigFromDir(h.Env.Root) 43 | ensure.Nil(t, err) 44 | 45 | ensure.DeepEqual(t, c.GetProjectConfig().Type, LegacyParseFormat) 46 | } 47 | 48 | func TestGetProjectRoot(t *testing.T) { 49 | t.Parallel() 50 | h := NewHarness(t) 51 | h.MakeEmptyRoot() 52 | defer h.Stop() 53 | 54 | ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse"), 0755)) 55 | ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse", "config"), 0755)) 56 | f, err := os.Create(filepath.Join(h.Env.Root, "parse", LegacyConfigFile)) 57 | ensure.Nil(t, err) 58 | defer f.Close() 59 | ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse", "cloud"), 0755)) 60 | ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, "parse", "public"), 0755)) 61 | ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, "parse", "cloud", "other", "config"), 0755)) 62 | 63 | ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, h.Env.Root), h.Env.Root) 64 | 65 | ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, filepath.Join(h.Env.Root, "parse", "config")), filepath.Join(h.Env.Root, "parse")) 66 | 67 | ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, filepath.Join(h.Env.Root, "parse", "cloud")), filepath.Join(h.Env.Root, "parse")) 68 | 69 | ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, filepath.Join(h.Env.Root, "parse", "public")), filepath.Join(h.Env.Root, "parse")) 70 | 71 | ensure.DeepEqual(t, GetLegacyProjectRoot(h.Env, filepath.Join(h.Env.Root, "parse", "cloud", "other")), filepath.Join(h.Env.Root, "parse")) 72 | } 73 | -------------------------------------------------------------------------------- /parsecli/context.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/facebookgo/parse" 8 | ) 9 | 10 | type Context struct { 11 | Config Config 12 | AppName string 13 | AppConfig AppConfig 14 | } 15 | 16 | func newContext(e *Env, appName string) (*Context, error) { 17 | config, err := ConfigFromDir(e.Root) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | app, err := config.App(appName) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | masterKey, err := app.GetMasterKey(e) 28 | if err != nil { 29 | return nil, err 30 | } 31 | e.ParseAPIClient = e.ParseAPIClient.WithCredentials( 32 | parse.MasterKey{ 33 | ApplicationID: app.GetApplicationID(), 34 | MasterKey: masterKey, 35 | }, 36 | ) 37 | 38 | if e.HerokuAPIClient != nil { 39 | authToken, err := app.GetApplicationAuth(e) 40 | if err != nil { 41 | return nil, err 42 | } 43 | headers := make(http.Header) 44 | headers.Add("Authorization", fmt.Sprintf("Bearer %s", authToken)) 45 | e.HerokuAPIClient.AdditionalHeaders = headers 46 | e.HerokuAPIClient.UserAgent = UserAgent 47 | } 48 | 49 | return &Context{ 50 | AppName: appName, 51 | AppConfig: app, 52 | Config: config, 53 | }, nil 54 | } 55 | -------------------------------------------------------------------------------- /parsecli/legacy_config.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/facebookgo/stackerr" 10 | ) 11 | 12 | const ConfigDir = "config" 13 | 14 | var LegacyConfigFile = filepath.Join(ConfigDir, "global.json") 15 | 16 | type legacyConfig struct { 17 | Global struct { 18 | ParseVersion string `json:"parseVersion,omitempty"` 19 | ParserEmail string `json:"email,omitempty"` 20 | } `json:"global,omitempty"` 21 | Applications map[string]*ParseAppConfig `json:"applications,omitempty"` 22 | } 23 | 24 | func readLegacyConfigFile(path string) (*legacyConfig, error) { 25 | f, err := os.Open(path) 26 | if err != nil { 27 | return nil, stackerr.Wrap(err) 28 | } 29 | defer f.Close() 30 | var c legacyConfig 31 | if err := json.NewDecoder(f).Decode(&c); err != nil { 32 | return nil, stackerr.Newf("Config file %q is not valid JSON.", path) 33 | } 34 | return &c, nil 35 | } 36 | 37 | func writeLegacyConfigFile(c *legacyConfig, path string) error { 38 | // if config directory does not exist yet, first create it 39 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { 40 | return stackerr.Wrap(err) 41 | } 42 | 43 | b, err := json.MarshalIndent(c, "", " ") 44 | if err != nil { 45 | return stackerr.Wrap(err) 46 | } 47 | return stackerr.Wrap(ioutil.WriteFile(path, b, 0600)) 48 | } 49 | 50 | func GetLegacyProjectRoot(e *Env, cur string) string { 51 | if _, err := os.Stat(filepath.Join(cur, LegacyConfigFile)); err == nil { 52 | return cur 53 | } 54 | 55 | root := cur 56 | base := filepath.Base(root) 57 | 58 | for base != "." && base != string(filepath.Separator) { 59 | base = filepath.Base(root) 60 | root = filepath.Dir(root) 61 | if base == "cloud" || base == "public" || base == "config" { 62 | if _, err := os.Stat(filepath.Join(root, LegacyConfigFile)); err == nil { 63 | return root 64 | } 65 | } 66 | } 67 | 68 | return cur 69 | } 70 | -------------------------------------------------------------------------------- /parsecli/legacy_config_test.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/facebookgo/ensure" 10 | ) 11 | 12 | var defaultParseConfig = &ParseConfig{ 13 | ProjectConfig: &ProjectConfig{ 14 | Type: LegacyParseFormat, 15 | Parse: &ParseProjectConfig{}, 16 | }, 17 | } 18 | 19 | func TestConfigAddAlias(t *testing.T) { 20 | t.Parallel() 21 | const name = "foo" 22 | const link = "bar" 23 | c := ParseConfig{Applications: map[string]*ParseAppConfig{link: {}}} 24 | ensure.Nil(t, c.AddAlias(name, link)) 25 | ensure.DeepEqual(t, c.Applications, map[string]*ParseAppConfig{ 26 | link: {}, 27 | name: {Link: link}, 28 | }) 29 | } 30 | 31 | func TestConfigAddAliasBadLink(t *testing.T) { 32 | t.Parallel() 33 | const name = "foo" 34 | const link = "bar" 35 | c := ParseConfig{} 36 | ensure.Err(t, c.AddAlias(name, link), regexp.MustCompile("wasn't found")) 37 | } 38 | 39 | func TestConfigAddAliasDupe(t *testing.T) { 40 | t.Parallel() 41 | const name = "foo" 42 | const link = "bar" 43 | c := ParseConfig{Applications: map[string]*ParseAppConfig{link: {}}} 44 | ensure.Nil(t, c.AddAlias(name, link)) 45 | ensure.Err(t, c.AddAlias(name, link), regexp.MustCompile("has already been added")) 46 | } 47 | 48 | func TestConfigSetDefault(t *testing.T) { 49 | t.Parallel() 50 | const name = "foo" 51 | c := ParseConfig{Applications: map[string]*ParseAppConfig{name: {}}} 52 | ensure.Nil(t, c.SetDefaultApp(name)) 53 | ensure.DeepEqual(t, c.Applications, map[string]*ParseAppConfig{ 54 | name: {}, 55 | DefaultKey: {Link: name}, 56 | }) 57 | } 58 | 59 | func TestConfigApp(t *testing.T) { 60 | t.Parallel() 61 | const name = "foo" 62 | c := ParseConfig{Applications: map[string]*ParseAppConfig{name: {}}} 63 | ac, err := c.App(name) 64 | ensure.Nil(t, err) 65 | ensure.DeepEqual(t, ac, &ParseAppConfig{}) 66 | } 67 | 68 | func TestConfigAppNotFound(t *testing.T) { 69 | t.Parallel() 70 | const name = "foo" 71 | c := ParseConfig{Applications: map[string]*ParseAppConfig{}} 72 | ac, err := c.App(name) 73 | ensure.True(t, ac == nil) 74 | ensure.Err(t, err, regexp.MustCompile(`App "foo" wasn't found`)) 75 | } 76 | 77 | func TestConfigAppDefaultNotFound(t *testing.T) { 78 | t.Parallel() 79 | c := ParseConfig{Applications: map[string]*ParseAppConfig{}} 80 | ac, err := c.App(DefaultKey) 81 | ensure.True(t, ac == nil) 82 | ensure.Err(t, err, regexp.MustCompile("No default app configured")) 83 | } 84 | 85 | func TestConfigAppLink(t *testing.T) { 86 | t.Parallel() 87 | const name = "foo" 88 | const link = "bar" 89 | expected := &ParseAppConfig{ApplicationID: "xyz"} 90 | c := ParseConfig{Applications: map[string]*ParseAppConfig{ 91 | link: expected, 92 | name: {Link: link}, 93 | }} 94 | ac, err := c.App(name) 95 | ensure.Nil(t, err) 96 | ensure.DeepEqual(t, ac, expected) 97 | } 98 | 99 | func TestConfigMissing(t *testing.T) { 100 | t.Parallel() 101 | dir, err := ioutil.TempDir("", "parse-cli-config-") 102 | ensure.Nil(t, err) 103 | defer os.RemoveAll(dir) 104 | c, err := ConfigFromDir(dir) 105 | ensure.True(t, c == nil) 106 | ensure.Err(t, err, regexp.MustCompile("Command must be run inside a Parse project.")) 107 | } 108 | 109 | func TestConfigGlobalMalformed(t *testing.T) { 110 | t.Parallel() 111 | dir := makeDirWithConfig(t, "foo") 112 | defer os.RemoveAll(dir) 113 | c, err := ConfigFromDir(dir) 114 | ensure.True(t, c == nil) 115 | ensure.Err(t, err, regexp.MustCompile("is not valid JSON")) 116 | } 117 | 118 | func TestConfigGlobalEmpty(t *testing.T) { 119 | t.Parallel() 120 | dir := makeDirWithConfig(t, "") 121 | defer os.RemoveAll(dir) 122 | c, err := ConfigFromDir(dir) 123 | ensure.True(t, c == nil) 124 | ensure.Err(t, err, regexp.MustCompile("is not valid JSON")) 125 | } 126 | 127 | func TestConfigEmptyGlobalOnly(t *testing.T) { 128 | t.Parallel() 129 | dir := makeDirWithConfig(t, "{}") 130 | defer os.RemoveAll(dir) 131 | c, err := ConfigFromDir(dir) 132 | ensure.Nil(t, err) 133 | ensure.DeepEqual(t, c.GetNumApps(), 0) 134 | } 135 | -------------------------------------------------------------------------------- /parsecli/login_test.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/facebookgo/ensure" 9 | ) 10 | 11 | func TestPopulateCreds(t *testing.T) { 12 | t.Parallel() 13 | 14 | h := NewHarness(t) 15 | defer h.Stop() 16 | 17 | l := &Login{} 18 | h.Env.In = strings.NewReader("email\npassword\n") 19 | ensure.Nil(t, l.populateCreds(h.Env)) 20 | ensure.DeepEqual(t, l.Credentials.Email, "email") 21 | ensure.DeepEqual(t, l.Credentials.Password, "password") 22 | } 23 | 24 | func TestGetTokenCredentials(t *testing.T) { 25 | t.Parallel() 26 | 27 | h := NewHarness(t) 28 | defer h.Stop() 29 | 30 | l := &Login{} 31 | h.Env.Server = "http://api.example.com/1/" 32 | 33 | l.TokenReader = strings.NewReader( 34 | `machine api.example.com 35 | login default 36 | password token 37 | `, 38 | ) 39 | _, credentials, err := l.GetTokenCredentials(h.Env, "") 40 | ensure.Nil(t, err) 41 | ensure.DeepEqual(t, credentials.Token, "token") 42 | 43 | l.TokenReader = strings.NewReader( 44 | `machine api.example.com 45 | login default 46 | password token 47 | `, 48 | ) 49 | h.Env.Server = "http://api.parse.com" 50 | _, credentials, err = l.GetTokenCredentials(h.Env, "") 51 | ensure.Err(t, err, regexp.MustCompile("Could not find account key")) 52 | 53 | l = &Login{} 54 | h.Env.Server = "http://api.example.com/1/" 55 | 56 | l.TokenReader = strings.NewReader( 57 | `machine api.example.com#email 58 | login default 59 | password token 60 | `, 61 | ) 62 | _, credentials, err = l.GetTokenCredentials(h.Env, "email") 63 | ensure.Nil(t, err) 64 | ensure.DeepEqual(t, credentials.Token, "token") 65 | 66 | l.TokenReader = strings.NewReader( 67 | `machine api.example.com#email 68 | login default 69 | password token 70 | `, 71 | ) 72 | h.Env.Server = "http://api.parse.com" 73 | _, credentials, err = l.GetTokenCredentials(h.Env, "email") 74 | ensure.Err(t, err, regexp.MustCompile("Could not find account key")) 75 | 76 | l = &Login{} 77 | h.Env.Server = "http://api.example.com/1/" 78 | 79 | l.TokenReader = strings.NewReader( 80 | `machine api.example.com#email 81 | login default 82 | password token1 83 | machine api.example.com 84 | login default 85 | password token2 86 | `, 87 | ) 88 | _, credentials, err = l.GetTokenCredentials(h.Env, "email") 89 | ensure.Nil(t, err) 90 | ensure.DeepEqual(t, credentials.Token, "token1") 91 | 92 | l.TokenReader = strings.NewReader( 93 | `machine api.example.com#email 94 | login default 95 | password token1 96 | machine api.example.com 97 | login default 98 | password token2 99 | `, 100 | ) 101 | 102 | _, credentials, err = l.GetTokenCredentials(h.Env, "xmail") 103 | ensure.Nil(t, err) 104 | ensure.DeepEqual(t, credentials.Token, "token2") 105 | } 106 | 107 | func TestAuthUserWithToken(t *testing.T) { 108 | t.Parallel() 109 | 110 | h := NewTokenHarness(t) 111 | defer h.Stop() 112 | 113 | l := &Login{} 114 | h.Env.ParserEmail = "email" 115 | h.Env.Server = "http://api.example.org/1/" 116 | 117 | l.TokenReader = strings.NewReader( 118 | `machine api.example.org#email 119 | login default 120 | password token 121 | `, 122 | ) 123 | _, err := l.AuthUserWithToken(h.Env, false) 124 | ensure.Nil(t, err) 125 | 126 | h.Env.ParserEmail = "email2" 127 | 128 | l.TokenReader = strings.NewReader( 129 | `machine api.example.org#email2 130 | login default 131 | password token 132 | `, 133 | ) 134 | _, err = l.AuthUserWithToken(h.Env, false) 135 | ensure.Err(t, err, regexp.MustCompile(`does not belong to "email2"`)) 136 | 137 | h.Env.ParserEmail = "" 138 | 139 | l.TokenReader = strings.NewReader( 140 | `machine api.example.org 141 | login default 142 | password token2 143 | `, 144 | ) 145 | _, err = l.AuthUserWithToken(h.Env, false) 146 | ensure.Err(t, err, regexp.MustCompile("provided is not valid")) 147 | 148 | } 149 | 150 | func TestUpdatedNetrcContent(t *testing.T) { 151 | t.Parallel() 152 | 153 | h := NewHarness(t) 154 | defer h.Stop() 155 | 156 | l := &Login{} 157 | 158 | h.Env.Server = "https://api.example.com/1/" 159 | updated, err := l.updatedNetrcContent( 160 | h.Env, 161 | strings.NewReader( 162 | `machine api.example.com#email 163 | login default 164 | password token0 165 | 166 | machine api.example.org 167 | login default 168 | password token 169 | `, 170 | ), 171 | "email", 172 | &Credentials{Token: "token"}, 173 | ) 174 | 175 | ensure.Nil(t, err) 176 | ensure.DeepEqual(t, 177 | string(updated), 178 | `machine api.example.com#email 179 | login default 180 | password token 181 | 182 | machine api.example.org 183 | login default 184 | password token 185 | `, 186 | ) 187 | 188 | h.Env.Server = "https://api.example.org/1/" 189 | updated, err = l.updatedNetrcContent(h.Env, 190 | strings.NewReader( 191 | `machine api.example.com 192 | login default 193 | password token 194 | `, 195 | ), 196 | "email", 197 | &Credentials{Token: "token"}, 198 | ) 199 | 200 | ensure.Nil(t, err) 201 | ensure.DeepEqual(t, 202 | string(updated), 203 | `machine api.example.com 204 | login default 205 | password token 206 | 207 | machine api.example.org#email 208 | login default 209 | password token`, 210 | ) 211 | } 212 | -------------------------------------------------------------------------------- /parsecli/parse_config.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "sort" 8 | 9 | "github.com/facebookgo/stackerr" 10 | ) 11 | 12 | type ParseProjectConfig struct { 13 | JSSDK string `json:"jssdk,omitempty"` 14 | } 15 | 16 | type ParseAppConfig struct { 17 | ApplicationID string `json:"applicationId,omitempty"` 18 | MasterKey string `json:"masterKey,omitempty"` 19 | Link string `json:"link,omitempty"` 20 | 21 | masterKey string 22 | } 23 | 24 | func (c *ParseAppConfig) WithInternalMasterKey(masterKey string) *ParseAppConfig { 25 | c.masterKey = masterKey 26 | return c 27 | } 28 | 29 | func (c *ParseAppConfig) GetApplicationID() string { 30 | return c.ApplicationID 31 | } 32 | 33 | func (c *ParseAppConfig) GetMasterKey(e *Env) (string, error) { 34 | if c.MasterKey != "" { 35 | return c.MasterKey, nil 36 | } 37 | if c.masterKey != "" { 38 | return c.masterKey, nil 39 | } 40 | app, err := FetchAppKeys(e, c.GetApplicationID()) 41 | if err != nil { 42 | return "", err 43 | } 44 | c.masterKey = app.MasterKey 45 | return app.MasterKey, nil 46 | } 47 | 48 | func (c *ParseAppConfig) GetApplicationAuth(e *Env) (string, error) { 49 | return c.GetMasterKey(e) 50 | } 51 | 52 | func (c *ParseAppConfig) GetLink() string { 53 | return c.Link 54 | } 55 | 56 | type ParseConfig struct { 57 | Applications map[string]*ParseAppConfig `json:"applications,omitempty"` 58 | ProjectConfig *ProjectConfig `json:"-"` 59 | } 60 | 61 | func (c *ParseConfig) AddAlias(name, link string) error { 62 | if _, found := c.Applications[name]; found { 63 | return stackerr.Newf("App %q has already been added.", name) 64 | } 65 | if _, found := c.Applications[link]; !found { 66 | return stackerr.Newf("App %q wasn't found.", link) 67 | } 68 | c.Applications[name] = &ParseAppConfig{Link: link} 69 | return nil 70 | } 71 | 72 | func (c *ParseConfig) SetDefaultApp(name string) error { 73 | delete(c.Applications, DefaultKey) 74 | return c.AddAlias(DefaultKey, name) 75 | } 76 | 77 | func (c *ParseConfig) App(name string) (AppConfig, error) { 78 | ac, found := c.Applications[name] 79 | if !found { 80 | if name == DefaultKey { 81 | return nil, stackerr.Newf("No default app configured.") 82 | } 83 | return nil, stackerr.Newf("App %q wasn't found.", name) 84 | } 85 | if ac.Link != "" { 86 | return c.App(ac.Link) 87 | } 88 | return ac, nil 89 | } 90 | 91 | func (c *ParseConfig) GetProjectConfig() *ProjectConfig { 92 | return c.ProjectConfig 93 | } 94 | 95 | func (c *ParseConfig) GetDefaultApp() string { 96 | var defaultApp string 97 | if DefaultKeyLink, ok := c.Applications[DefaultKey]; ok { 98 | defaultApp = DefaultKeyLink.Link 99 | } 100 | return defaultApp 101 | } 102 | 103 | func (c *ParseConfig) GetNumApps() int { 104 | return len(c.Applications) 105 | } 106 | 107 | func (c *ParseConfig) PrettyPrintApps(e *Env) { 108 | apps := c.Applications 109 | 110 | defaultApp := c.GetDefaultApp() 111 | 112 | var appNames []string 113 | for appName := range apps { 114 | appNames = append(appNames, appName) 115 | } 116 | sort.Strings(appNames) 117 | 118 | if len(appNames) == 0 { 119 | return 120 | } 121 | 122 | fmt.Fprintln( 123 | e.Out, 124 | "The following apps are associated with Cloud Code in the current directory:", 125 | ) 126 | 127 | for _, appName := range appNames { 128 | if appName == DefaultKey { 129 | continue 130 | } 131 | if defaultApp == appName { 132 | fmt.Fprint(e.Out, "* ") 133 | } else { 134 | fmt.Fprint(e.Out, " ") 135 | } 136 | fmt.Fprintf(e.Out, "%s", appName) 137 | 138 | if config, _ := apps[appName]; config.GetLink() != "" { 139 | fmt.Fprintf(e.Out, " -> %s", config.GetLink()) 140 | } 141 | fmt.Fprintln(e.Out, "") 142 | } 143 | } 144 | 145 | func readParseConfigFile(path string) (*ParseConfig, error) { 146 | f, err := os.Open(path) 147 | if err != nil { 148 | return nil, stackerr.Wrap(err) 149 | } 150 | defer f.Close() 151 | var c ParseConfig 152 | if err := json.NewDecoder(f).Decode(&c); err != nil { 153 | return nil, stackerr.Newf("Config file %q is not valid JSON.", path) 154 | } 155 | return &c, nil 156 | } 157 | 158 | func setParseDefault(e *Env, newDefault, defaultApp string, parseConfig *ParseConfig) error { 159 | apps := parseConfig.Applications 160 | if _, ok := apps[newDefault]; !ok { 161 | parseConfig.PrettyPrintApps(e) 162 | return stackerr.Newf("Invalid application name \"%s\". Please select from the valid applications printed above.", newDefault) 163 | } 164 | 165 | if defaultApp == "" { 166 | apps[DefaultKey] = &ParseAppConfig{Link: newDefault} 167 | } else { 168 | apps[DefaultKey].Link = newDefault 169 | } 170 | if err := StoreConfig(e, parseConfig); err != nil { 171 | return err 172 | } 173 | fmt.Fprintf(e.Out, "Default app set to %s.\n", newDefault) 174 | return nil 175 | } 176 | -------------------------------------------------------------------------------- /parsecli/runners.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type cobraRun func(cmd *cobra.Command, args []string) 11 | 12 | // RunNoArgs wraps a run function that shouldn't get any arguments. 13 | func RunNoArgs(e *Env, f func(*Env) error) cobraRun { 14 | return func(cmd *cobra.Command, args []string) { 15 | if len(args) != 0 { 16 | fmt.Fprintf(e.Err, "unexpected arguments:%+v\n\n", args) 17 | cmd.Help() 18 | e.Exit(1) 19 | } 20 | if err := f(e); err != nil { 21 | fmt.Fprintln(e.Err, ErrorString(e, err)) 22 | e.Exit(1) 23 | } 24 | } 25 | } 26 | 27 | // RunWithArgs wraps a run function that can access arguments to cobraCmd 28 | func RunWithArgs(e *Env, f func(*Env, []string) error) cobraRun { 29 | return func(cmd *cobra.Command, args []string) { 30 | if err := f(e, args); err != nil { 31 | fmt.Fprintln(e.Err, ErrorString(e, err)) 32 | e.Exit(1) 33 | } 34 | } 35 | } 36 | 37 | // RunWithClient wraps a run function that should get an app, when the default is 38 | // picked from the config in the current working directory. 39 | func RunWithClient(e *Env, f func(*Env, *Context) error) cobraRun { 40 | return func(cmd *cobra.Command, args []string) { 41 | app := DefaultKey 42 | if len(args) > 1 { 43 | fmt.Fprintf( 44 | e.Err, 45 | "unexpected arguments, only an optional app name is expected:%+v\n\n", 46 | args, 47 | ) 48 | cmd.Help() 49 | e.Exit(1) 50 | } 51 | if len(args) == 1 { 52 | app = args[0] 53 | } 54 | cl, err := newContext(e, app) 55 | if err != nil { 56 | fmt.Fprintln(e.Err, ErrorString(e, err)) 57 | e.Exit(1) 58 | } 59 | if err := f(e, cl); err != nil { 60 | fmt.Fprintln(e.Err, ErrorString(e, err)) 61 | e.Exit(1) 62 | } 63 | } 64 | } 65 | 66 | // RunWithClientConfirm wraps a run function that takes an app name 67 | // or asks for confirmation to use the default app name instead 68 | func RunWithClientConfirm(e *Env, f func(*Env, *Context) error) cobraRun { 69 | return func(cmd *cobra.Command, args []string) { 70 | app := DefaultKey 71 | if len(args) > 1 { 72 | fmt.Fprintf( 73 | e.Err, 74 | "unexpected arguments, only an optional app name is expected:%+v\n\n", 75 | args, 76 | ) 77 | cmd.Help() 78 | e.Exit(1) 79 | } 80 | if len(args) == 1 { 81 | app = args[0] 82 | } 83 | cl, err := newContext(e, app) 84 | if err != nil { 85 | fmt.Fprintln(e.Err, ErrorString(e, err)) 86 | e.Exit(1) 87 | } 88 | 89 | if app == DefaultKey { 90 | fmt.Fprintf( 91 | e.Out, 92 | `Did not provide app name as an argument to the command. 93 | Please enter an app name to execute this command on 94 | or press ENTER to use the default app %q: `, 95 | cl.Config.GetDefaultApp(), 96 | ) 97 | var appName string 98 | fmt.Fscanf(e.In, "%s\n", &appName) 99 | appName = strings.TrimSpace(appName) 100 | if appName != "" { 101 | cl, err = newContext(e, appName) 102 | if err != nil { 103 | fmt.Fprintln(e.Err, ErrorString(e, err)) 104 | e.Exit(1) 105 | } 106 | } 107 | } 108 | 109 | if err := f(e, cl); err != nil { 110 | fmt.Fprintln(e.Err, ErrorString(e, err)) 111 | e.Exit(1) 112 | } 113 | } 114 | } 115 | 116 | // RunWithArgsClient wraps a run function that should get an app, whee the default is 117 | // picked from the config in the current working directory. It also passes args to the 118 | // runner function 119 | func RunWithArgsClient(e *Env, f func(*Env, *Context, []string) error) cobraRun { 120 | return func(cmd *cobra.Command, args []string) { 121 | app := DefaultKey 122 | if len(args) > 1 { 123 | app = args[0] 124 | args = args[1:] 125 | } 126 | cl, err := newContext(e, app) 127 | if err != nil { 128 | fmt.Fprintln(e.Err, ErrorString(e, err)) 129 | e.Exit(1) 130 | } 131 | if err := f(e, cl, args); err != nil { 132 | fmt.Fprintln(e.Err, ErrorString(e, err)) 133 | e.Exit(1) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /parsecli/runners_test.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/facebookgo/ensure" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func noOpCmd() *cobra.Command { 13 | var c cobra.Command 14 | c.SetOutput(ioutil.Discard) 15 | return &c 16 | } 17 | 18 | func TestRunNoArgsFuncError(t *testing.T) { 19 | t.Parallel() 20 | const message = "hello world" 21 | h := NewHarness(t) 22 | defer h.Stop() 23 | h.Env.Exit = func(i int) { panic(exitCode(i)) } 24 | func() { 25 | defer ensure.PanicDeepEqual(t, exitCode(1)) 26 | r := RunNoArgs(h.Env, func(*Env) error { return errors.New(message) }) 27 | r(noOpCmd(), nil) 28 | }() 29 | ensure.StringContains(t, h.Err.String(), message) 30 | } 31 | 32 | func TestRunWithAppMultipleArgs(t *testing.T) { 33 | t.Parallel() 34 | h := NewHarness(t) 35 | defer h.Stop() 36 | h.Env.Exit = func(i int) { panic(exitCode(i)) } 37 | func() { 38 | defer ensure.PanicDeepEqual(t, exitCode(1)) 39 | r := RunWithClient(h.Env, nil) 40 | r(noOpCmd(), []string{"foo", "bar"}) 41 | }() 42 | ensure.StringContains( 43 | t, 44 | h.Err.String(), 45 | "only an optional app name is expected", 46 | ) 47 | } 48 | 49 | func TestRunWithAppNonProjectDir(t *testing.T) { 50 | t.Parallel() 51 | h := NewHarness(t) 52 | defer h.Stop() 53 | h.MakeEmptyRoot() 54 | h.Env.Exit = func(i int) { panic(exitCode(i)) } 55 | func() { 56 | defer ensure.PanicDeepEqual(t, exitCode(1)) 57 | r := RunWithClient(h.Env, nil) 58 | r(noOpCmd(), nil) 59 | }() 60 | ensure.StringContains( 61 | t, 62 | h.Err.String(), 63 | "Command must be run inside a Parse project.", 64 | ) 65 | } 66 | 67 | func TestRunWithAppNotFound(t *testing.T) { 68 | t.Parallel() 69 | h := NewHarness(t) 70 | defer h.Stop() 71 | c := legacyConfig{Applications: map[string]*ParseAppConfig{"a": {}}} 72 | h.MakeWithConfig(jsonStr(t, c)) 73 | h.Env.Exit = func(i int) { panic(exitCode(i)) } 74 | func() { 75 | defer ensure.PanicDeepEqual(t, exitCode(1)) 76 | r := RunWithClient(h.Env, nil) 77 | r(noOpCmd(), []string{"b"}) 78 | }() 79 | ensure.StringContains(t, h.Err.String(), `App "b" wasn't found`) 80 | } 81 | 82 | func TestRunWithAppNamed(t *testing.T) { 83 | t.Parallel() 84 | h := NewHarness(t) 85 | defer h.Stop() 86 | const appName = "a" 87 | c := &ParseConfig{ 88 | Applications: map[string]*ParseAppConfig{ 89 | appName: {ApplicationID: "id", MasterKey: "token"}, 90 | }, 91 | } 92 | h.MakeWithConfig(jsonStr(t, c)) 93 | r := RunWithClient(h.Env, func(e *Env, c *Context) error { 94 | ensure.NotNil(t, c) 95 | return nil 96 | }) 97 | r(noOpCmd(), []string{"a"}) 98 | } 99 | 100 | func TestRunWithDefault(t *testing.T) { 101 | t.Parallel() 102 | h := NewHarness(t) 103 | defer h.Stop() 104 | const appName = "a" 105 | c := &ParseConfig{ 106 | Applications: map[string]*ParseAppConfig{ 107 | appName: {ApplicationID: "id", MasterKey: "token"}, 108 | DefaultKey: {Link: appName}, 109 | }, 110 | } 111 | h.MakeWithConfig(jsonStr(t, c)) 112 | r := RunWithClient(h.Env, func(e *Env, c *Context) error { 113 | ensure.NotNil(t, c) 114 | return nil 115 | }) 116 | r(noOpCmd(), []string{"a"}) 117 | } 118 | 119 | func TestRunWithAppError(t *testing.T) { 120 | t.Parallel() 121 | h := NewHarness(t) 122 | defer h.Stop() 123 | const appName = "a" 124 | c := &ParseConfig{ 125 | Applications: map[string]*ParseAppConfig{ 126 | appName: {ApplicationID: "id", MasterKey: "token"}, 127 | }, 128 | } 129 | h.MakeWithConfig(jsonStr(t, c)) 130 | h.Env.Exit = func(i int) { panic(exitCode(i)) } 131 | const message = "hello world" 132 | func() { 133 | defer ensure.PanicDeepEqual(t, exitCode(1)) 134 | r := RunWithClient(h.Env, func(e *Env, c *Context) error { 135 | ensure.NotNil(t, c) 136 | return errors.New(message) 137 | }) 138 | r(noOpCmd(), []string{"a"}) 139 | }() 140 | ensure.StringContains(t, h.Err.String(), message) 141 | } 142 | -------------------------------------------------------------------------------- /parsecli/utils.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "strings" 11 | 12 | "github.com/facebookgo/errgroup" 13 | "github.com/facebookgo/parse" 14 | "github.com/facebookgo/stackerr" 15 | ) 16 | 17 | const ( 18 | SampleSource = ` 19 | // Use Parse.Cloud.define to define as many cloud functions as you want. 20 | // For example: 21 | Parse.Cloud.define("hello", function(request, response) { 22 | response.success("Hello world!"); 23 | }); 24 | ` 25 | 26 | SampleHTML = ` 27 | 28 |
29 |To get started, edit this file at public/index.html and start adding static content.
43 |If you want something a bit more dynamic, delete this file and check out our hosting docs.
44 |<%= message %>
45 | 46 | 47 | ` 48 | 49 | helloJade = ` 50 | doctype 5 51 | html 52 | head 53 | title Sample App 54 | body 55 | h1 Hello World 56 | p= message 57 | ` 58 | ) 59 | -------------------------------------------------------------------------------- /parsecmd/utils.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | ) 7 | 8 | var numbers = regexp.MustCompile("[0-9]+") 9 | 10 | func numericLessThan(a, b string) bool { 11 | aLen, bLen := len(a), len(b) 12 | minLen := aLen 13 | if bLen < minLen { 14 | minLen = bLen 15 | } 16 | if minLen == 0 { 17 | if aLen == 0 { 18 | return bLen != 0 19 | } 20 | return false 21 | } 22 | 23 | i := 0 24 | for ; i < minLen && a[i] == b[i]; i++ { 25 | } 26 | if i == minLen { 27 | if aLen == bLen { 28 | return false 29 | } 30 | if aLen == minLen { 31 | return true 32 | } 33 | return false 34 | } 35 | 36 | a, b = a[i:], b[i:] 37 | aNos, bNos := numbers.FindAllStringIndex(a, 1), numbers.FindAllStringIndex(b, 1) 38 | 39 | if len(aNos) == 1 && aNos[0][0] == 0 && 40 | len(bNos) == 1 && bNos[0][0] == 0 { 41 | aNum, err := strconv.Atoi(a[:aNos[0][1]]) 42 | if err != nil { 43 | return a < b 44 | } 45 | bNum, err := strconv.Atoi(b[:bNos[0][1]]) 46 | if err != nil { 47 | return a < b 48 | } 49 | if aNum != bNum { 50 | return aNum < bNum 51 | } 52 | } 53 | return a < b 54 | } 55 | 56 | type naturalStrings []string 57 | 58 | func (v naturalStrings) Len() int { 59 | return len(v) 60 | } 61 | 62 | func (v naturalStrings) Less(i, j int) bool { 63 | return !numericLessThan(v[i], v[j]) // desc order 64 | } 65 | 66 | func (v naturalStrings) Swap(i, j int) { 67 | v[i], v[j] = v[j], v[i] 68 | } 69 | -------------------------------------------------------------------------------- /parsecmd/utils_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | 7 | "github.com/facebookgo/ensure" 8 | ) 9 | 10 | func TestBothEmpty(t *testing.T) { 11 | t.Parallel() 12 | ensure.False(t, numericLessThan("", "")) 13 | } 14 | 15 | func TestOnlyAEmpty(t *testing.T) { 16 | t.Parallel() 17 | ensure.True(t, numericLessThan("", "b")) 18 | } 19 | 20 | func TestOnlyBEmpty(t *testing.T) { 21 | t.Parallel() 22 | ensure.False(t, numericLessThan("a", "")) 23 | } 24 | 25 | func TestBothEqual(t *testing.T) { 26 | t.Parallel() 27 | ensure.False(t, numericLessThan("abc", "abc")) 28 | } 29 | 30 | func TestAPrefixOfB(t *testing.T) { 31 | t.Parallel() 32 | ensure.True(t, numericLessThan("abc", "abcd")) 33 | } 34 | 35 | func TestBPrefixOfA(t *testing.T) { 36 | t.Parallel() 37 | ensure.False(t, numericLessThan("abcd", "abc")) 38 | } 39 | 40 | func TestABSansNumbers(t *testing.T) { 41 | t.Parallel() 42 | 43 | const max = 10 44 | dictionary := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 45 | var rb [max]byte 46 | randomString := func(length int) string { 47 | bytes := rb[:length] 48 | _, err := rand.Read(bytes) 49 | ensure.Nil(t, err) 50 | for k, v := range bytes { 51 | bytes[k] = dictionary[v%byte(len(dictionary))] 52 | } 53 | return string(bytes) 54 | } 55 | 56 | for i := 1; i < max; i++ { 57 | for j := 1; j < max; j++ { 58 | a, b := randomString(i), randomString(j) 59 | ensure.DeepEqual(t, numericLessThan(a, b), a < b) 60 | } 61 | } 62 | } 63 | 64 | func TestABWithNumbers(t *testing.T) { 65 | ensure.True(t, numericLessThan("abc9", "abc12")) 66 | ensure.False(t, numericLessThan("abc12", "abc9")) 67 | ensure.DeepEqual(t, numericLessThan("abc001", "abc1"), "abc001" < "abc1") 68 | } 69 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "runtime" 8 | 9 | "github.com/ParsePlatform/parse-cli/parsecli" 10 | "github.com/facebookgo/stackerr" 11 | "github.com/inconshreveable/go-update" 12 | "github.com/kardianos/osext" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | const ( 17 | macDownload = "parse" 18 | windowsDownload = "parse.exe" 19 | linuxDownload = "parse_linux" 20 | linuxArmDownload = "parse_linux_arm" 21 | downloadURLFormat = "https://github.com/ParsePlatform/parse-cli/releases/download/release_%s/%s" 22 | ) 23 | 24 | type updateCmd struct{} 25 | 26 | func (u *updateCmd) latestVersion(e *parsecli.Env) (string, error) { 27 | v := make(url.Values) 28 | v.Set("version", "latest") 29 | req := &http.Request{ 30 | Method: "GET", 31 | URL: &url.URL{Path: "supported", RawQuery: v.Encode()}, 32 | } 33 | 34 | var res struct { 35 | Version string `json:"version"` 36 | } 37 | 38 | if _, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { 39 | return "", stackerr.Wrap(err) 40 | } 41 | 42 | return res.Version, nil 43 | } 44 | 45 | func (u *updateCmd) getDownloadURL(e *parsecli.Env) (string, error) { 46 | ostype := runtime.GOOS 47 | arch := runtime.GOARCH 48 | 49 | latestVersion, err := u.latestVersion(e) 50 | if err != nil { 51 | return "", err 52 | } 53 | if latestVersion == "" || latestVersion == parsecli.Version { 54 | return "", nil 55 | } 56 | 57 | var downloadURL string 58 | switch ostype { 59 | case "darwin": 60 | downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, macDownload) 61 | case "windows": 62 | downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, windowsDownload) 63 | case "linux": 64 | if arch == "arm" { 65 | downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, linuxArmDownload) 66 | } else { 67 | downloadURL = fmt.Sprintf(downloadURLFormat, latestVersion, linuxDownload) 68 | } 69 | } 70 | return downloadURL, nil 71 | } 72 | 73 | func (u *updateCmd) updateCLI(e *parsecli.Env) (bool, error) { 74 | downloadURL, err := u.getDownloadURL(e) 75 | if err != nil { 76 | return false, err 77 | } 78 | if downloadURL == "" { 79 | return false, nil 80 | } 81 | exec, err := osext.Executable() 82 | if err != nil { 83 | return false, stackerr.Wrap(err) 84 | } 85 | 86 | fmt.Fprintf(e.Out, "Downloading binary from %s.\n", downloadURL) 87 | resp, err := http.Get(downloadURL) 88 | if err != nil { 89 | return false, stackerr.Newf("Update failed with error: %v", err) 90 | } 91 | defer resp.Body.Close() 92 | err = update.Apply(resp.Body, update.Options{TargetPath: exec}) 93 | if err != nil { 94 | return false, stackerr.Newf("Update failed with error: %v", err) 95 | } 96 | fmt.Fprintf(e.Out, "Successfully updated binary at: %s\n", exec) 97 | return true, nil 98 | } 99 | 100 | func (u *updateCmd) run(e *parsecli.Env) error { 101 | updated, err := u.updateCLI(e) 102 | if err != nil { 103 | return err 104 | } 105 | if !updated { 106 | fmt.Fprintf(e.Out, "Already using the latest cli version: %s\n", parsecli.Version) 107 | } 108 | return nil 109 | } 110 | 111 | func NewUpdateCmd(e *parsecli.Env) *cobra.Command { 112 | var u updateCmd 113 | return &cobra.Command{ 114 | Use: "update", 115 | Short: "Updates this tool to the latest version", 116 | Long: "Updates this tool to the latest version.", 117 | Run: parsecli.RunNoArgs(e, u.run), 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /update_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/ParsePlatform/parse-cli/parsecli" 9 | "github.com/facebookgo/ensure" 10 | "github.com/facebookgo/jsonpipe" 11 | "github.com/facebookgo/parse" 12 | ) 13 | 14 | func TestLatestVersion(t *testing.T) { 15 | t.Parallel() 16 | 17 | h := parsecli.NewHarness(t) 18 | defer h.Stop() 19 | 20 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 21 | ensure.DeepEqual(t, r.URL.Path, "/1/supported") 22 | return &http.Response{ 23 | StatusCode: http.StatusOK, 24 | Body: ioutil.NopCloser( 25 | jsonpipe.Encode( 26 | map[string]string{"version": "2.0.2"}, 27 | ), 28 | ), 29 | }, nil 30 | }) 31 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 32 | u := new(updateCmd) 33 | 34 | latestVersion, err := u.latestVersion(h.Env) 35 | ensure.Nil(t, err) 36 | ensure.DeepEqual(t, latestVersion, "2.0.2") 37 | 38 | downloadURL, err := u.getDownloadURL(h.Env) 39 | ensure.StringContains(t, 40 | downloadURL, "https://github.com/ParsePlatform/parse-cli/releases/download/release_2.0.2") 41 | } 42 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | 9 | "github.com/ParsePlatform/parse-cli/parsecli" 10 | "github.com/facebookgo/stackerr" 11 | ) 12 | 13 | func getConfirmation(message string, e *parsecli.Env) bool { 14 | fmt.Fprintf(e.Out, message) 15 | var confirm string 16 | fmt.Fscanf(e.In, "%s\n", &confirm) 17 | lower := strings.ToLower(confirm) 18 | return lower != "" && strings.HasPrefix(lower, "y") 19 | } 20 | 21 | func validateURL(urlStr string) error { 22 | netURL, err := url.Parse(urlStr) 23 | if err != nil { 24 | return stackerr.Wrap(err) 25 | } 26 | 27 | if netURL.Scheme != "https" { 28 | return errors.New("Please enter a valid https url") 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ParsePlatform/parse-cli/parsecli" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | type versionCmd struct{} 11 | 12 | func (c *versionCmd) run(e *parsecli.Env) error { 13 | fmt.Fprintln(e.Out, parsecli.Version) 14 | return nil 15 | } 16 | 17 | func NewVersionCmd(e *parsecli.Env) *cobra.Command { 18 | var c versionCmd 19 | cmd := &cobra.Command{ 20 | Use: "version", 21 | Short: "Gets the Command Line Tools version", 22 | Long: `Gets the Command Line Tools version.`, 23 | Run: parsecli.RunNoArgs(e, c.run), 24 | Aliases: []string{"cliversion"}, 25 | } 26 | return cmd 27 | } 28 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ParsePlatform/parse-cli/parsecli" 7 | "github.com/facebookgo/ensure" 8 | ) 9 | 10 | func TestVersion(t *testing.T) { 11 | t.Parallel() 12 | h := parsecli.NewHarness(t) 13 | defer h.Stop() 14 | var c versionCmd 15 | err := c.run(h.Env) 16 | ensure.Nil(t, err) 17 | ensure.DeepEqual(t, h.Out.String(), parsecli.Version+"\n") 18 | ensure.DeepEqual(t, h.Err.String(), "") 19 | } 20 | --------------------------------------------------------------------------------