├── .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 | [![Build Status](https://travis-ci.org/parse-community/parse-cli.svg?branch=master)](https://travis-ci.org/parse-community/parse-cli) 6 | [![Join The Conversation](https://img.shields.io/discourse/https/community.parseplatform.org/topics.svg)](https://community.parseplatform.org/c/parse-server) 7 | [![Backers on Open Collective](https://opencollective.com/parse-server/backers/badge.svg)][open-collective-link] 8 | [![Sponsors on Open Collective](https://opencollective.com/parse-server/sponsors/badge.svg)][open-collective-link] 9 | [![License][license-svg]][license-link] 10 | [![Twitter Follow](https://img.shields.io/twitter/follow/ParsePlatform.svg?label=Follow%20us%20on%20Twitter&style=social)](https://twitter.com/intent/follow?screen_name=ParsePlatform) 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 | My ParseApp site 30 | 38 | 39 | 40 |
41 |

Congratulations! You're already hosting with Parse.

42 |

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 |
45 | 46 | 47 | ` 48 | ) 49 | 50 | func getHostFromURL(urlStr, email string) (string, error) { 51 | netURL, err := url.Parse(urlStr) 52 | if err != nil { 53 | return "", stackerr.Wrap(err) 54 | } 55 | server := regexp.MustCompile(`(.*):\d+$`).ReplaceAllString(netURL.Host, "$1") 56 | if server == "" { 57 | return "", stackerr.Newf("%s is not a valid url", urlStr) 58 | } 59 | if email != "" { 60 | return fmt.Sprintf("%s#%s", server, email), nil 61 | } 62 | return server, nil 63 | } 64 | 65 | func Last4(str string) string { 66 | l := len(str) 67 | if l > 4 { 68 | return fmt.Sprintf("%s%s", strings.Repeat("*", l-4), str[l-4:l]) 69 | } 70 | return str 71 | } 72 | 73 | // errorString returns the error string with our without the stack trace 74 | // depending on the Environment variable. this exists because we want plain 75 | // messages for end users, but when we're working on the CLI we want the stack 76 | // trace for debugging. 77 | func ErrorString(e *Env, err error) string { 78 | type hasUnderlying interface { 79 | HasUnderlying() error 80 | } 81 | 82 | parseErr := func(err error) error { 83 | if apiErr, ok := err.(*parse.Error); ok { 84 | return errors.New(apiErr.Message) 85 | } 86 | return err 87 | } 88 | 89 | lastErr := func(err error) error { 90 | if serr, ok := err.(*stackerr.Error); ok { 91 | if errs := stackerr.Underlying(serr); len(errs) != 0 { 92 | err = errs[len(errs)-1] 93 | } 94 | } else { 95 | if eu, ok := err.(hasUnderlying); ok { 96 | err = eu.HasUnderlying() 97 | } 98 | } 99 | 100 | return parseErr(err) 101 | } 102 | 103 | if !e.ErrorStack { 104 | if merr, ok := err.(errgroup.MultiError); ok { 105 | var multiError []error 106 | for _, ierr := range []error(merr) { 107 | multiError = append(multiError, lastErr(ierr)) 108 | } 109 | err = errgroup.MultiError(multiError) 110 | } else { 111 | err = lastErr(err) 112 | } 113 | return parseErr(err).Error() 114 | } 115 | 116 | return err.Error() 117 | } 118 | 119 | func CreateConfigWithContent(path, content string) error { 120 | file, err := os.OpenFile( 121 | path, 122 | os.O_RDWR|os.O_CREATE|os.O_TRUNC, 123 | 0600, 124 | ) 125 | if err != nil && !os.IsExist(err) { 126 | return stackerr.Wrap(err) 127 | } 128 | defer file.Close() 129 | if _, err := file.WriteString(content); err != nil { 130 | return stackerr.Wrap(err) 131 | } 132 | if err := file.Close(); err != nil { 133 | return stackerr.Wrap(err) 134 | } 135 | return nil 136 | } 137 | 138 | var NewProjectFiles = []struct { 139 | Dirname, Filename, Content string 140 | }{ 141 | {"cloud", "main.js", SampleSource}, 142 | {"public", "index.html", SampleHTML}, 143 | } 144 | 145 | func CloneSampleCloudCode(e *Env, dumpTemplate bool) error { 146 | err := os.MkdirAll(e.Root, 0755) 147 | if err != nil { 148 | return stackerr.Wrap(err) 149 | } 150 | 151 | err = CreateConfigWithContent( 152 | filepath.Join(e.Root, ParseProject), 153 | fmt.Sprintf( 154 | `{ 155 | "project_type" : %d, 156 | "parse": {"jssdk":""} 157 | }`, 158 | ParseFormat, 159 | ), 160 | ) 161 | if err != nil { 162 | return err 163 | } 164 | err = CreateConfigWithContent( 165 | filepath.Join(e.Root, ParseLocal), 166 | "{}", 167 | ) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | // no need to set up the template code 173 | if !dumpTemplate { 174 | return nil 175 | } 176 | 177 | for _, info := range NewProjectFiles { 178 | sampleDir := filepath.Join(e.Root, info.Dirname) 179 | if _, err := os.Stat(sampleDir); err != nil { 180 | if !os.IsNotExist(err) { 181 | return stackerr.Wrap(err) 182 | } 183 | if err := os.Mkdir(sampleDir, 0755); err != nil { 184 | return stackerr.Wrap(err) 185 | } 186 | } 187 | 188 | sampleFile := filepath.Join(sampleDir, info.Filename) 189 | if _, err := os.Stat(sampleFile); err != nil { 190 | if os.IsNotExist(err) { 191 | file, err := os.Create(sampleFile) 192 | if err != nil && !os.IsExist(err) { 193 | return stackerr.Wrap(err) 194 | } 195 | 196 | defer file.Close() 197 | 198 | if _, err := file.WriteString(info.Content); err != nil { 199 | return stackerr.Wrap(err) 200 | } 201 | if err := file.Close(); err != nil { 202 | return stackerr.Wrap(err) 203 | } 204 | } 205 | } 206 | } 207 | return nil 208 | } 209 | -------------------------------------------------------------------------------- /parsecli/utils_test.go: -------------------------------------------------------------------------------- 1 | package parsecli 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/facebookgo/ensure" 8 | "github.com/facebookgo/errgroup" 9 | "github.com/facebookgo/parse" 10 | "github.com/facebookgo/stackerr" 11 | ) 12 | 13 | func TestGetHostFromURL(t *testing.T) { 14 | urlStr := "https://api.parse.com/1/" 15 | host, err := getHostFromURL(urlStr, "") 16 | ensure.Nil(t, err) 17 | ensure.DeepEqual(t, host, "api.parse.com") 18 | 19 | urlStr = "https://api.parse.com/1/" 20 | host, err = getHostFromURL(urlStr, "yolo@earth.com") 21 | ensure.Nil(t, err) 22 | ensure.DeepEqual(t, host, "api.parse.com#yolo@earth.com") 23 | 24 | urlStr = "https://api.example.com:8080/1/" 25 | host, err = getHostFromURL(urlStr, "") 26 | ensure.Nil(t, err) 27 | ensure.DeepEqual(t, host, "api.example.com") 28 | 29 | urlStr = "https://api.example.com:8080/1/" 30 | host, err = getHostFromURL(urlStr, "yolo@earth.com") 31 | ensure.Nil(t, err) 32 | ensure.DeepEqual(t, host, "api.example.com#yolo@earth.com") 33 | 34 | urlStr = "api.example.com:8080:90" 35 | host, err = getHostFromURL(urlStr, "") 36 | ensure.Err(t, err, regexp.MustCompile("not a valid url")) 37 | } 38 | 39 | func TestLastFour(t *testing.T) { 40 | t.Parallel() 41 | 42 | ensure.DeepEqual(t, Last4(""), "") 43 | ensure.DeepEqual(t, Last4("a"), "a") 44 | ensure.DeepEqual(t, Last4("ab"), "ab") 45 | ensure.DeepEqual(t, Last4("abc"), "abc") 46 | ensure.DeepEqual(t, Last4("abcd"), "abcd") 47 | ensure.DeepEqual(t, Last4("abcdefg"), "***defg") 48 | } 49 | 50 | func TestErrorStringWithoutStack(t *testing.T) { 51 | t.Parallel() 52 | h := NewHarness(t) 53 | defer h.Stop() 54 | h.Env.ErrorStack = false 55 | const message = "hello world" 56 | actual := ErrorString(h.Env, stackerr.New(message)) 57 | ensure.StringContains(t, actual, message) 58 | ensure.StringDoesNotContain(t, actual, ".go") 59 | } 60 | 61 | type exitCode int 62 | 63 | func TestErrorStringWithStack(t *testing.T) { 64 | t.Parallel() 65 | h := NewHarness(t) 66 | defer h.Stop() 67 | h.Env.ErrorStack = true 68 | const message = "hello world" 69 | actual := ErrorString(h.Env, stackerr.New(message)) 70 | ensure.StringContains(t, actual, message) 71 | ensure.StringContains(t, actual, ".go") 72 | } 73 | 74 | func TestErrorString(t *testing.T) { 75 | t.Parallel() 76 | h := NewHarness(t) 77 | defer h.Stop() 78 | apiErr := &parse.Error{Code: -1, Message: "Error\nMessage"} 79 | ensure.DeepEqual(t, `Error 80 | Message`, 81 | ErrorString(h.Env, apiErr), 82 | ) 83 | } 84 | 85 | func TestStackErrorString(t *testing.T) { 86 | t.Parallel() 87 | 88 | h := NewHarness(t) 89 | defer h.Stop() 90 | 91 | err := stackerr.New("error") 92 | 93 | h.Env.ErrorStack = false 94 | errStr := ErrorString(h.Env, err) 95 | 96 | ensure.DeepEqual(t, errStr, "error") 97 | 98 | h.Env.ErrorStack = true 99 | errStr = ErrorString(h.Env, err) 100 | 101 | ensure.StringContains(t, errStr, "error") 102 | ensure.StringContains(t, errStr, ".go") 103 | 104 | err = stackerr.Wrap(&parse.Error{Message: "message", Code: 1}) 105 | h.Env.ErrorStack = false 106 | errStr = ErrorString(h.Env, err) 107 | 108 | ensure.DeepEqual(t, errStr, "message") 109 | 110 | h.Env.ErrorStack = true 111 | errStr = ErrorString(h.Env, err) 112 | 113 | ensure.StringContains(t, errStr, `parse: api error with code=1 and message="message`) 114 | ensure.StringContains(t, errStr, ".go") 115 | } 116 | 117 | func TestMultiErrorString(t *testing.T) { 118 | t.Parallel() 119 | 120 | h := NewHarness(t) 121 | defer h.Stop() 122 | 123 | err := errgroup.MultiError( 124 | []error{ 125 | stackerr.New("error"), 126 | stackerr.Wrap(&parse.Error{Message: "message", Code: 1}), 127 | }, 128 | ) 129 | 130 | h.Env.ErrorStack = false 131 | errStr := ErrorString(h.Env, err) 132 | 133 | ensure.DeepEqual(t, errStr, "multiple errors: error | message") 134 | 135 | h.Env.ErrorStack = true 136 | errStr = ErrorString(h.Env, err) 137 | 138 | ensure.StringContains(t, errStr, "multiple errors") 139 | ensure.StringContains(t, errStr, `parse: api error with code=1 and message="message"`) 140 | ensure.StringContains(t, errStr, ".go") 141 | } 142 | -------------------------------------------------------------------------------- /parsecmd/Resources/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /parsecmd/Resources/Test.xcarchive/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ApplicationProperties 6 | 7 | ApplicationPath 8 | Applications/Test.app 9 | CFBundleIdentifier 10 | com.facebook.Test 11 | CFBundleShortVersionString 12 | 1.0 13 | CFBundleVersion 14 | 1.0 15 | SigningIdentity 16 | iPhone Developer: Nikita Lutsenko (8KB4VANCMF) 17 | 18 | ArchiveVersion 19 | 2 20 | CreationDate 21 | 2014-08-04T19:46:06Z 22 | Name 23 | Test 24 | SchemeName 25 | Test 26 | 27 | 28 | -------------------------------------------------------------------------------- /parsecmd/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleIdentifier 8 | com.apple.xcode.dsym.com.facebook.Test 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundlePackageType 12 | dSYM 13 | CFBundleSignature 14 | ???? 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1.0 19 | 20 | 21 | -------------------------------------------------------------------------------- /parsecmd/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Resources/DWARF/Test: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parse-community/parse-cli/eec3be4d6a766064ae17bc316b6125da2970a038/parsecmd/Resources/Test.xcarchive/dSYMs/Test.app.dSYM/Contents/Resources/DWARF/Test -------------------------------------------------------------------------------- /parsecmd/Resources/build_tools/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/parse-community/parse-cli/eec3be4d6a766064ae17bc316b6125da2970a038/parsecmd/Resources/build_tools/.empty -------------------------------------------------------------------------------- /parsecmd/Resources/mapping.txt: -------------------------------------------------------------------------------- 1 | #such 2 | -------------------------------------------------------------------------------- /parsecmd/add.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ParsePlatform/parse-cli/parsecli" 7 | "github.com/facebookgo/stackerr" 8 | ) 9 | 10 | func GetParseAppConfig(app *parsecli.App) *parsecli.ParseAppConfig { 11 | pc := &parsecli.ParseAppConfig{ 12 | ApplicationID: app.ApplicationID, 13 | } 14 | return pc.WithInternalMasterKey(app.MasterKey) 15 | } 16 | 17 | func AddSelectedParseApp( 18 | appName string, 19 | appConfig *parsecli.ParseAppConfig, 20 | args []string, 21 | makeDefault, verbose bool, 22 | e *parsecli.Env, 23 | ) error { 24 | config, err := parsecli.ConfigFromDir(e.Root) 25 | if err != nil { 26 | return err 27 | } 28 | parseConfig, ok := config.(*parsecli.ParseConfig) 29 | if !ok { 30 | return stackerr.New("Invalid Cloud Code config.") 31 | } 32 | 33 | // add app to config 34 | if _, ok := parseConfig.Applications[appName]; ok { 35 | return stackerr.Newf("App %s has already been added", appName) 36 | } 37 | 38 | parseConfig.Applications[appName] = appConfig 39 | 40 | if len(args) > 0 && args[0] != "" { 41 | alias := args[0] 42 | aliasConfig, ok := parseConfig.Applications[alias] 43 | if !ok { 44 | parseConfig.Applications[alias] = &parsecli.ParseAppConfig{Link: appName} 45 | } 46 | if ok && aliasConfig.GetLink() != "" { 47 | fmt.Fprintf(e.Out, "Overwriting alias: %q to point to %q\n", alias, appName) 48 | parseConfig.Applications[alias] = &parsecli.ParseAppConfig{Link: appName} 49 | } 50 | } 51 | 52 | if makeDefault { 53 | if _, ok := parseConfig.Applications[parsecli.DefaultKey]; ok { 54 | return stackerr.New(`Default key already set. To override default, use command "parse default"`) 55 | } 56 | parseConfig.Applications[parsecli.DefaultKey] = &parsecli.ParseAppConfig{Link: appName} 57 | } 58 | 59 | if err := parsecli.StoreConfig(e, parseConfig); err != nil { 60 | return err 61 | } 62 | if verbose { 63 | fmt.Fprintf(e.Out, "Written config for %q\n", appName) 64 | if makeDefault { 65 | fmt.Fprintf(e.Out, "Set %q as default\n", appName) 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /parsecmd/android_symbol_uploader.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "fmt" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "regexp" 11 | "strconv" 12 | 13 | "github.com/ParsePlatform/parse-cli/parsecli" 14 | "github.com/facebookgo/stackerr" 15 | ) 16 | 17 | var androidCodeversion = regexp.MustCompile(`versionCode='(\d+)'`) 18 | 19 | type androidSymbolUploader struct { 20 | Path string 21 | Apk string 22 | Manifest string 23 | AAPT string 24 | } 25 | 26 | func findAAPT(root string) []string { 27 | var aapts []string 28 | // we ignore the error returned by filepath.Walk because we are only making 29 | // a best eeffort search for aapt, if there is an error for any reason, 30 | // we just ignore it & forgo the search 31 | filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 32 | if err != nil { 33 | return err 34 | } 35 | if info != nil && filepath.Base(path) == "aapt" { 36 | aapts = append(aapts, path) 37 | } 38 | return nil 39 | }) 40 | return aapts 41 | } 42 | 43 | func (a *androidSymbolUploader) validate() error { 44 | if (a.Apk == "") == (a.Manifest == "") { 45 | return stackerr.New("You need to supply one of either the apk or manifest file.") 46 | } 47 | return nil 48 | } 49 | 50 | func (a *androidSymbolUploader) acceptsPath() bool { 51 | return filepath.Base(a.Path) == "mapping.txt" 52 | } 53 | 54 | func (a *androidSymbolUploader) getAAPT(androidHome string) (string, error) { 55 | options := []string{a.AAPT} 56 | // either in $ANDROID_HOME/platform-tools/aapt 57 | options = append(options, filepath.Join(androidHome, "platform_tools", "aapt")) 58 | 59 | // or $ANDROID_HOME/build-tools//aapt 60 | buildTools := filepath.Join(androidHome, "build_tools") 61 | options = append(options, findAAPT(buildTools)...) 62 | 63 | for _, o := range options { 64 | if out, err := exec.Command(o).Output(); err == nil { 65 | if bytes.HasPrefix(out, []byte("Android Asset Packaging Tool")) { 66 | return o, nil 67 | } 68 | } 69 | } 70 | return "", stackerr.New("Cannot find aapt, you might need to set ANDROID_HOME.") 71 | } 72 | 73 | func (a *androidSymbolUploader) getBuildVersionFromManifest() (int, error) { 74 | if a.Manifest == "" { 75 | return 0, stackerr.New("Please provide manifest file path.") 76 | } 77 | var version struct { 78 | XMLName xml.Name `xml:"manifest"` 79 | VersionCode int `xml:"http://schemas.android.com/apk/res/android versionCode,attr"` 80 | } 81 | manifest, err := os.Open(a.Manifest) 82 | if err != nil { 83 | return 0, stackerr.Wrap(err) 84 | } 85 | defer manifest.Close() 86 | if err := xml.NewDecoder(manifest).Decode(&version); err != nil { 87 | return 0, stackerr.Wrap(err) 88 | } 89 | if version.VersionCode == 0 { 90 | return 0, stackerr.New("Manifest does not contain build version.") 91 | } 92 | return version.VersionCode, nil 93 | } 94 | 95 | func (a *androidSymbolUploader) getBuildVersionFromAPK() (int, error) { 96 | if a.Apk == "" { 97 | return 0, stackerr.New("Please provide apk file path.") 98 | } 99 | if a.AAPT == "" { 100 | androidHome := os.Getenv("ANDROID_HOME") 101 | if androidHome == "" { 102 | androidHome = os.Getenv("ANDROID_SDK") 103 | } 104 | if androidHome == "" { 105 | return 0, stackerr.New("Cannot find aapt, you might need to set ANDROID_HOME.") 106 | } 107 | 108 | aapt, err := a.getAAPT(androidHome) 109 | if err != nil { 110 | return 0, err 111 | } 112 | a.AAPT = aapt 113 | } 114 | 115 | bytes, err := exec.Command(a.AAPT, "dump", "badging", a.Apk).Output() 116 | if err != nil { 117 | return 0, stackerr.Wrap(err) 118 | } 119 | return parseVersionFromBytes(bytes) 120 | } 121 | 122 | func parseVersionFromBytes(bytes []byte) (int, error) { 123 | v := androidCodeversion.FindAllSubmatch(bytes, 1) 124 | if len(v) == 0 { 125 | return 0, stackerr.New("Cannot determine build version.") 126 | } 127 | version, err := strconv.Atoi(string(v[0][1])) 128 | if err != nil { 129 | return 0, stackerr.Wrap(err) 130 | } 131 | return int(version), nil 132 | } 133 | 134 | func (a *androidSymbolUploader) getBuildVersion(e *parsecli.Env) (int, error) { 135 | var handleError = func(versionCode int, err error) (int, error) { 136 | if err != nil { 137 | return 0, err 138 | } 139 | if versionCode == 1 { 140 | fmt.Fprintln(e.Out, "Warning: build number is '1'.") 141 | } 142 | return versionCode, nil 143 | } 144 | 145 | if a.Manifest != "" { 146 | return handleError(a.getBuildVersionFromManifest()) 147 | } 148 | 149 | return handleError(a.getBuildVersionFromAPK()) 150 | } 151 | 152 | func (a *androidSymbolUploader) uploadSymbols(e *parsecli.Env) error { 153 | appBuildVersion, err := a.getBuildVersion(e) 154 | if err != nil { 155 | return err 156 | } 157 | commonHeaders := map[string]string{ 158 | "X-Parse-App-Build-Version": fmt.Sprintf("%d", appBuildVersion), 159 | } 160 | return uploadSymbolFiles([]string{a.Path}, commonHeaders, false, e) 161 | } 162 | -------------------------------------------------------------------------------- /parsecmd/android_symbol_uploader_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "testing" 10 | 11 | "github.com/ParsePlatform/parse-cli/parsecli" 12 | "github.com/facebookgo/ensure" 13 | ) 14 | 15 | func TestMain(m *testing.M) { 16 | if filepath.Base(os.Args[0]) == "aapt" { 17 | fmt.Fprintln(os.Stdout, `Android Asset Packaging Tool 18 | versionCode='2778' 19 | `) 20 | os.Exit(0) 21 | } 22 | 23 | aaptBin := filepath.Join("Resources", "build_tools", "aapt") 24 | 25 | bin, err := os.Create(aaptBin) 26 | if err != nil { 27 | panic(err) 28 | } 29 | defer bin.Close() 30 | 31 | cur, err := os.Open(os.Args[0]) 32 | if err != nil { 33 | panic(err) 34 | } 35 | defer cur.Close() 36 | 37 | if _, err := io.Copy(bin, cur); err != nil { 38 | panic(err) 39 | } 40 | if err := os.Chmod(aaptBin, 0755); err != nil { 41 | panic(err) 42 | } 43 | 44 | if err := bin.Close(); err != nil { 45 | panic(err) 46 | } 47 | 48 | retCode := m.Run() 49 | 50 | if err := os.Remove(aaptBin); err != nil { 51 | panic(err) 52 | } 53 | os.Exit(retCode) 54 | } 55 | 56 | func TestParseVersionFromBytes(t *testing.T) { 57 | t.Parallel() 58 | 59 | _, err := parseVersionFromBytes([]byte(`versionCode='a'`)) 60 | ensure.NotNil(t, err) 61 | 62 | tests := []struct { 63 | input string 64 | expected int 65 | }{ 66 | {`versionCode='1'`, 1}, 67 | {`versionCode='12'`, 12}, 68 | {`versionCode='1234'`, 1234}, 69 | {`versionCode='12345678'`, 12345678}, 70 | } 71 | 72 | for _, test := range tests { 73 | actual, err := parseVersionFromBytes([]byte(test.input)) 74 | ensure.Nil(t, err) 75 | ensure.DeepEqual(t, actual, test.expected) 76 | } 77 | } 78 | 79 | func TestAndroidSymbolUploaderValidate(t *testing.T) { 80 | t.Parallel() 81 | var a androidSymbolUploader 82 | 83 | ensure.Err(t, a.validate(), regexp.MustCompile( 84 | "You need to supply one of either the apk or manifest file.")) 85 | 86 | a.Apk = "a" 87 | a.Manifest = "m" 88 | ensure.Err(t, a.validate(), regexp.MustCompile( 89 | "You need to supply one of either the apk or manifest file.")) 90 | 91 | a.Apk = "" 92 | a.Manifest = "m" 93 | ensure.Nil(t, a.validate()) 94 | 95 | a.Manifest = "" 96 | a.Apk = "a" 97 | ensure.Nil(t, a.validate()) 98 | } 99 | 100 | func TestAndroidAcceptsPath(t *testing.T) { 101 | t.Parallel() 102 | a := androidSymbolUploader{ 103 | Path: filepath.Join("Resources", "mapping.txt"), 104 | } 105 | ensure.True(t, a.acceptsPath()) 106 | } 107 | 108 | func TestFindAAPT(t *testing.T) { 109 | t.Parallel() 110 | h := parsecli.NewHarness(t) 111 | defer h.Stop() 112 | 113 | aapts := findAAPT("Resources") 114 | ensure.DeepEqual(t, aapts[:], []string{filepath.Join("Resources", "build_tools", "aapt")}) 115 | } 116 | 117 | func TestBuildVersionFromManifest(t *testing.T) { 118 | t.Parallel() 119 | a := androidSymbolUploader{ 120 | Path: filepath.Join("Resources", "mapping.txt"), 121 | Manifest: filepath.Join("Resources", "AndroidManifest.xml"), 122 | } 123 | 124 | v, err := a.getBuildVersionFromManifest() 125 | ensure.Nil(t, err) 126 | ensure.DeepEqual(t, v, 2778) 127 | } 128 | 129 | func TestBuildVersionFromApk(t *testing.T) { 130 | t.Parallel() 131 | a := androidSymbolUploader{ 132 | Path: filepath.Join("Resources", "mapping.txt"), 133 | Apk: "test", 134 | AAPT: filepath.Join("Resources", "build_tools", "aapt"), 135 | } 136 | 137 | v, err := a.getBuildVersionFromAPK() 138 | ensure.Nil(t, err) 139 | ensure.DeepEqual(t, v, 2778) 140 | } 141 | 142 | func TestGetAAPT(t *testing.T) { 143 | t.Parallel() 144 | a := androidSymbolUploader{ 145 | Path: filepath.Join("Resources", "mapping.txt"), 146 | } 147 | 148 | path, err := a.getAAPT("Resources") 149 | ensure.Nil(t, err) 150 | ensure.DeepEqual(t, path, filepath.Join("Resources", "build_tools", "aapt")) 151 | } 152 | 153 | func TestGetBuildVersion(t *testing.T) { 154 | t.Parallel() 155 | a := androidSymbolUploader{ 156 | Path: filepath.Join("Resources", "mapping.txt"), 157 | Manifest: filepath.Join("Resources", "AndroidManifest.xml"), 158 | Apk: "test", 159 | AAPT: filepath.Join("Resources", "build_tools", "aapt"), 160 | } 161 | 162 | h := parsecli.NewHarness(t) 163 | defer h.Stop() 164 | 165 | manifestVersion, err := a.getBuildVersion(h.Env) 166 | ensure.Nil(t, err) 167 | ensure.DeepEqual(t, manifestVersion, 2778) 168 | 169 | a.Manifest = "" 170 | apkVersion, err := a.getBuildVersion(h.Env) 171 | ensure.Nil(t, err) 172 | ensure.DeepEqual(t, apkVersion, 2778) 173 | } 174 | -------------------------------------------------------------------------------- /parsecmd/develop.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "github.com/ParsePlatform/parse-cli/parsecli" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | const ( 13 | maxLogRetries = 50 14 | developFollowNumLogs = 25 15 | networkErrorsWait = 20 16 | otherErrorsWait = 10 17 | ) 18 | 19 | type developCmd struct { 20 | deployInterval time.Duration // The number of seconds between deploy 21 | mustFetch bool // If set, prevDeployInfo will always be fetched from server 22 | Verbose bool // If set, will print details about deploy in addition to server logs 23 | } 24 | 25 | type deployFunc func(parseVersion string, 26 | prevDeplInfo *deployInfo, 27 | forDevelop bool, 28 | e *parsecli.Env) (*deployInfo, error) 29 | 30 | func (d *developCmd) contDeploy(e *parsecli.Env, deployer deployFunc, first, done chan struct{}) { 31 | if d.deployInterval == 0 { 32 | d.deployInterval = time.Second 33 | } 34 | 35 | var prevDeplInfo *deployInfo 36 | ticker := e.Clock.Ticker(d.deployInterval) 37 | defer ticker.Stop() 38 | 39 | latestError := false 40 | for { 41 | select { 42 | case <-ticker.C: 43 | case <-done: 44 | return 45 | } 46 | config, err := parsecli.ConfigFromDir(e.Root) 47 | if err != nil { 48 | if !latestError { 49 | latestError = true 50 | } 51 | if latestError { 52 | fmt.Fprintf( 53 | e.Err, 54 | `Config malformed. 55 | Please fix your config file in %s and try again. 56 | `, 57 | parsecli.GetConfigFile(e), 58 | ) 59 | } 60 | if first != nil { // first deploy aborted 61 | close(first) 62 | first = nil 63 | } 64 | continue 65 | } 66 | latestError = false 67 | newDeplInfo, _ := deployer(config.GetProjectConfig().Parse.JSSDK, prevDeplInfo, true, e) 68 | if !d.mustFetch { 69 | prevDeplInfo = newDeplInfo 70 | } 71 | if first != nil { // first deploy finished 72 | close(first) 73 | first = nil 74 | } 75 | } 76 | } 77 | 78 | func (d *developCmd) handleError(e *parsecli.Env, err error, sleep func(time.Duration)) error { 79 | if err == nil { 80 | return nil 81 | } 82 | 83 | if _, ok := err.(*net.OpError); ok { 84 | fmt.Fprintf( 85 | e.Err, 86 | "Flaky network. Waiting %ds before trying to fetch logs again.", 87 | networkErrorsWait, 88 | ) 89 | sleep(networkErrorsWait * time.Second) 90 | } else { 91 | sleep(otherErrorsWait * time.Second) 92 | } 93 | return err 94 | } 95 | 96 | func (d *developCmd) run(e *parsecli.Env, c *parsecli.Context) error { 97 | first := make(chan struct{}) 98 | go d.contDeploy(e, 99 | deployFunc((&deployCmd{Verbose: d.Verbose}).deploy), 100 | first, 101 | make(chan struct{})) 102 | <-first 103 | 104 | var err error 105 | for i := 0; i < maxLogRetries; i++ { 106 | l := &logsCmd{num: 1, level: "INFO"} 107 | 108 | // we want to fetch only the latest log line after first deploy 109 | // there after num is reset to 25 in log tailer for efficiency 110 | err = d.handleError(e, l.run(e, c), e.Clock.Sleep) 111 | if err != nil { 112 | continue 113 | } 114 | 115 | l.num = developFollowNumLogs 116 | l.follow = true 117 | err = d.handleError(e, l.run(e, c), e.Clock.Sleep) 118 | if err == nil { 119 | return nil 120 | } 121 | } 122 | return err 123 | } 124 | 125 | func NewDevelopCmd(e *parsecli.Env) *cobra.Command { 126 | d := &developCmd{deployInterval: time.Second} 127 | cmd := &cobra.Command{ 128 | Use: "develop app", 129 | Short: "Monitors for changes to code and deploys, also tails parse logs", 130 | Long: `Monitors for changes to source files and uploads updated files to Parse. ` + 131 | `This will also monitor the parse INFO log for any new log messages and write ` + 132 | `out updates to the terminal. This requires an app to be provided, to ` + 133 | `avoid running develop on production apps accidently.`, 134 | Run: parsecli.RunWithClientConfirm(e, d.run), 135 | } 136 | cmd.Flags().DurationVarP(&d.deployInterval, "interval", "i", d.deployInterval, "Number of seconds between deploys.") 137 | cmd.Flags().BoolVarP(&d.mustFetch, "fetch", "f", d.mustFetch, "Always fetch previous deployment info from server") 138 | cmd.Flags().BoolVarP(&d.Verbose, "verbose", "v", d.Verbose, "Control verbosity of cmd line logs") 139 | 140 | return cmd 141 | } 142 | -------------------------------------------------------------------------------- /parsecmd/develop_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "net" 8 | "path/filepath" 9 | "testing" 10 | "time" 11 | 12 | "github.com/ParsePlatform/parse-cli/parsecli" 13 | "github.com/facebookgo/ensure" 14 | ) 15 | 16 | func TestContDeploy(t *testing.T) { 17 | t.Parallel() 18 | 19 | h := createParseProject(t) 20 | defer h.Stop() 21 | 22 | deployer := deployFunc(func(parseVersion string, 23 | prevDeplInfo *deployInfo, 24 | forDevelop bool, 25 | e *parsecli.Env) (*deployInfo, error) { 26 | return &deployInfo{}, nil 27 | }) 28 | 29 | done := make(chan struct{}) 30 | go func() { 31 | h.Clock.Add(time.Second) 32 | close(done) 33 | }() 34 | 35 | first := make(chan struct{}) 36 | (&developCmd{}).contDeploy(h.Env, deployer, first, done) 37 | _, opened := <-first 38 | ensure.False(t, opened) 39 | } 40 | 41 | func TestContDeployConfigErr(t *testing.T) { 42 | t.Parallel() 43 | 44 | h := createParseProject(t) 45 | defer h.Stop() 46 | 47 | ensure.Nil(t, 48 | ioutil.WriteFile( 49 | filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), 50 | []byte("}"), 51 | 0600, 52 | ), 53 | ) 54 | h.Env.Type = parsecli.LegacyParseFormat 55 | 56 | deployer := deployFunc(func(parseVersion string, 57 | prevDeplInfo *deployInfo, 58 | forDevelop bool, 59 | e *parsecli.Env) (*deployInfo, error) { 60 | return &deployInfo{}, nil 61 | }) 62 | 63 | done := make(chan struct{}) 64 | go func() { 65 | h.Clock.Add(5 * time.Second) 66 | close(done) 67 | }() 68 | 69 | first := make(chan struct{}) 70 | (&developCmd{}).contDeploy(h.Env, deployer, first, done) 71 | _, opened := <-first 72 | ensure.False(t, opened) 73 | 74 | ensure.StringContains( 75 | t, 76 | h.Err.String(), 77 | fmt.Sprintf( 78 | `Config malformed. 79 | Please fix your config file in %s and try again. 80 | `, 81 | filepath.Join(h.Env.Root, parsecli.LegacyConfigFile), 82 | ), 83 | ) 84 | } 85 | 86 | func TestHandleError(t *testing.T) { 87 | t.Parallel() 88 | h := parsecli.NewHarness(t) 89 | defer h.Stop() 90 | 91 | d := &developCmd{} 92 | netErr := &net.OpError{Err: errors.New("network")} 93 | sleep := func(d time.Duration) {} 94 | ensure.DeepEqual(t, d.handleError(h.Env, netErr, sleep), netErr) 95 | ensure.DeepEqual(t, h.Err.String(), "Flaky network. Waiting 20s before trying to fetch logs again.") 96 | 97 | h.Out.Reset() 98 | h.Err.Reset() 99 | otherErr := errors.New("other") 100 | ensure.DeepEqual(t, d.handleError(h.Env, otherErr, sleep), otherErr) 101 | ensure.DeepEqual(t, h.Err.Len(), 0) 102 | } 103 | -------------------------------------------------------------------------------- /parsecmd/download_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "regexp" 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 | scriptPath = regexp.MustCompile("/1/scripts/.*") 19 | hostedPath = regexp.MustCompile("/1/hosted_files/.*") 20 | ) 21 | 22 | func newDownloadHarness(t testing.TB) (*parsecli.Harness, *downloadCmd) { 23 | h := createParseProject(t) 24 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 25 | ensure.DeepEqual(t, r.FormValue("version"), "version") 26 | switch { 27 | case scriptPath.MatchString(r.URL.Path): 28 | return &http.Response{ 29 | StatusCode: http.StatusOK, 30 | Body: ioutil.NopCloser(strings.NewReader(`"content"`)), 31 | }, nil 32 | case hostedPath.MatchString(r.URL.Path): 33 | return &http.Response{ 34 | StatusCode: http.StatusOK, 35 | Body: ioutil.NopCloser(strings.NewReader(`[1, 1, 2, 3, 5, 8, 13]`)), 36 | }, nil 37 | default: 38 | return &http.Response{ 39 | StatusCode: http.StatusNotFound, 40 | Body: ioutil.NopCloser(strings.NewReader(`{"error": "something is wrong"}`)), 41 | }, nil 42 | } 43 | }) 44 | 45 | h.MakeEmptyRoot() 46 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 47 | 48 | d := &downloadCmd{ 49 | release: &deployInfo{ 50 | Versions: deployFileData{ 51 | Cloud: map[string]string{"main.js": "version"}, 52 | Public: map[string]string{"index.html": "version"}, 53 | }, 54 | Checksums: deployFileData{ 55 | Cloud: map[string]string{"main.js": "9a0364b9e99bb480dd25e1f0284c8555"}, 56 | Public: map[string]string{"index.html": "ea46dea1ca5f0b7a728aa3c2a87ae8a1"}, 57 | }, 58 | }, 59 | } 60 | 61 | return h, d 62 | } 63 | 64 | func TestDownload(t *testing.T) { 65 | t.Parallel() 66 | 67 | h, d := newDownloadHarness(t) 68 | defer h.Stop() 69 | 70 | tempDir, err := ioutil.TempDir("", "download_") 71 | ensure.Nil(t, err) 72 | defer func() { 73 | os.RemoveAll(tempDir) 74 | }() 75 | 76 | err = d.download(h.Env, tempDir, d.release) 77 | ensure.Nil(t, err) 78 | for file := range d.release.Versions.Cloud { 79 | _, err := os.Open(filepath.Join(tempDir, parsecli.CloudDir, file)) 80 | ensure.Nil(t, err) 81 | } 82 | for file := range d.release.Versions.Public { 83 | _, err := os.Open(filepath.Join(tempDir, parsecli.HostingDir, file)) 84 | ensure.Nil(t, err) 85 | } 86 | } 87 | 88 | func TestDownloadMoveFiles(t *testing.T) { 89 | t.Parallel() 90 | h, d := newDownloadHarness(t) 91 | defer h.Stop() 92 | 93 | content := []byte("content") 94 | d.release.Checksums.Cloud["main.js"] = "9a0364b9e99bb480dd25e1f0284c8555" 95 | d.release.Checksums.Public["index.html"] = "9a0364b9e99bb480dd25e1f0284c8555" 96 | 97 | tempDir, err := ioutil.TempDir("", "move_files_") 98 | defer func() { 99 | os.RemoveAll(tempDir) 100 | }() 101 | 102 | err = os.MkdirAll( 103 | filepath.Join(tempDir, parsecli.HostingDir), 104 | 0755, 105 | ) 106 | ensure.Nil(t, err) 107 | err = os.MkdirAll( 108 | filepath.Join(tempDir, parsecli.CloudDir), 109 | 0755, 110 | ) 111 | ensure.Nil(t, err) 112 | 113 | err = ioutil.WriteFile( 114 | filepath.Join(tempDir, parsecli.HostingDir, "index.html"), 115 | content, 116 | 0644, 117 | ) 118 | ensure.Nil(t, err) 119 | err = ioutil.WriteFile( 120 | filepath.Join(tempDir, parsecli.CloudDir, "main.js"), 121 | content, 122 | 0644, 123 | ) 124 | ensure.Nil(t, err) 125 | 126 | err = d.moveFiles(h.Env, tempDir, d.release) 127 | ensure.Nil(t, err) 128 | 129 | readData, err := ioutil.ReadFile( 130 | filepath.Join(h.Env.Root, parsecli.HostingDir, "index.html"), 131 | ) 132 | ensure.DeepEqual(t, readData, content) 133 | readData, err = ioutil.ReadFile( 134 | filepath.Join(h.Env.Root, parsecli.CloudDir, "main.js"), 135 | ) 136 | ensure.DeepEqual(t, readData, content) 137 | } 138 | -------------------------------------------------------------------------------- /parsecmd/generate.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/ParsePlatform/parse-cli/parsecli" 11 | "github.com/facebookgo/stackerr" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | const ( 16 | expressEjs = "express-ejs" 17 | expressJade = "express-jade" 18 | comma = ", " 19 | ) 20 | 21 | var validTypes = map[string]bool{ 22 | expressEjs: true, 23 | expressJade: true, 24 | } 25 | 26 | type generateCmd struct { 27 | generateType string 28 | } 29 | 30 | func (g *generateCmd) validateArgs() error { 31 | if _, ok := validTypes[g.generateType]; !ok { 32 | var buf bytes.Buffer 33 | for key := range validTypes { 34 | buf.WriteString(key) 35 | buf.WriteString(comma) 36 | } 37 | return stackerr.Newf("type can only be one of {%s}", buf.String()) 38 | } 39 | return nil 40 | } 41 | 42 | func (g *generateCmd) run(e *parsecli.Env) error { 43 | if err := g.validateArgs(); err != nil { 44 | return err 45 | } 46 | 47 | destDir := filepath.Join(e.Root, parsecli.CloudDir) 48 | existingFiles := false 49 | 50 | appJs := filepath.Join(destDir, "app.js") 51 | if _, err := os.Stat(appJs); err == nil { 52 | fmt.Fprintf(e.Err, "%s already exists.\n", appJs) 53 | existingFiles = true 54 | } else { 55 | if !os.IsNotExist(err) { 56 | return stackerr.Wrap(err) 57 | } 58 | } 59 | 60 | viewsDir := filepath.Join(destDir, "views") 61 | if _, err := os.Stat(viewsDir); err != nil { 62 | if !os.IsNotExist(err) { 63 | return stackerr.Wrap(err) 64 | } 65 | fmt.Fprintf(e.Out, "Creating directory %s.\n", viewsDir) 66 | if err := os.MkdirAll(viewsDir, 0755); err != nil { 67 | return stackerr.Wrap(err) 68 | } 69 | } 70 | 71 | var helloHTMLTemplate string 72 | switch g.generateType { 73 | case expressJade: 74 | helloHTMLTemplate = filepath.Join(viewsDir, "hello.jade") 75 | case expressEjs: 76 | helloHTMLTemplate = filepath.Join(viewsDir, "hello.ejs") 77 | } 78 | 79 | if _, err := os.Stat(helloHTMLTemplate); err == nil { 80 | fmt.Fprintf(e.Err, "%s already exists.\n", helloHTMLTemplate) 81 | existingFiles = true 82 | } else { 83 | if !os.IsNotExist(err) { 84 | return stackerr.Wrap(err) 85 | } 86 | } 87 | 88 | if existingFiles { 89 | return stackerr.New("Please remove the above existing files and try again.") 90 | } 91 | 92 | fmt.Fprintf(e.Out, "Writing out sample file %s\n", appJs) 93 | file, err := os.Create(appJs) 94 | if err != nil && !os.IsExist(err) { 95 | return stackerr.Wrap(err) 96 | } 97 | 98 | defer file.Close() 99 | 100 | switch g.generateType { 101 | case expressJade: 102 | if _, err := file.WriteString(strings.Replace(sampleAppJS, "ejs", "jade", -1)); err != nil { 103 | return stackerr.Wrap(err) 104 | } 105 | case expressEjs: 106 | if _, err := file.WriteString(sampleAppJS); err != nil { 107 | return stackerr.Wrap(err) 108 | } 109 | } 110 | if err := file.Close(); err != nil { 111 | return stackerr.Wrap(err) 112 | } 113 | 114 | fmt.Fprintf(e.Out, "Writing out sample file %s\n", helloHTMLTemplate) 115 | file, err = os.Create(helloHTMLTemplate) 116 | if err != nil && !os.IsExist(err) { 117 | return stackerr.Wrap(err) 118 | } 119 | 120 | defer file.Close() 121 | 122 | switch g.generateType { 123 | case expressJade: 124 | if _, err := file.WriteString(helloJade); err != nil { 125 | return stackerr.Wrap(err) 126 | } 127 | case expressEjs: 128 | if _, err := file.WriteString(helloEJS); err != nil { 129 | return stackerr.Wrap(err) 130 | } 131 | } 132 | if err := file.Close(); err != nil { 133 | return stackerr.Wrap(err) 134 | } 135 | 136 | fmt.Fprintf(e.Out, "\nAlmost done! Please add this line to the top of your main.js:\n\n") 137 | fmt.Fprintf(e.Out, "\trequire('cloud/app.js');\n\n") 138 | return nil 139 | } 140 | 141 | func NewGenerateCmd(e *parsecli.Env) *cobra.Command { 142 | c := generateCmd{} 143 | cmd := &cobra.Command{ 144 | Use: "generate", 145 | Short: "Generates a sample express app in the current project directory", 146 | Long: "Generates a sample express app in the current project directory.", 147 | Run: parsecli.RunNoArgs(e, c.run), 148 | } 149 | cmd.Flags().StringVarP(&c.generateType, "type", "t", expressEjs, "Type of templates to use for generation.") 150 | return cmd 151 | } 152 | -------------------------------------------------------------------------------- /parsecmd/generate_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 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 TestGenerateCheckValidArgs(t *testing.T) { 16 | t.Parallel() 17 | g := &generateCmd{} 18 | for arg := range validTypes { 19 | g.generateType = arg 20 | err := g.validateArgs() 21 | ensure.Nil(t, err) 22 | } 23 | } 24 | 25 | func TestGenerateCheckInvalidArgs(t *testing.T) { 26 | t.Parallel() 27 | g := generateCmd{generateType: "x"} 28 | err := g.validateArgs() 29 | ensure.Err(t, err, regexp.MustCompile(`type can only be one of `)) 30 | } 31 | 32 | func newGenerateCmdHarness(t testing.TB, generateType string) (*parsecli.Harness, *generateCmd) { 33 | h := parsecli.NewHarness(t) 34 | h.MakeEmptyRoot() 35 | g := &generateCmd{generateType} 36 | return h, g 37 | } 38 | 39 | func TestGenerateEjsOutput(t *testing.T) { 40 | t.Parallel() 41 | h, g := newGenerateCmdHarness(t, expressEjs) 42 | defer h.Stop() 43 | g.run(h.Env) 44 | 45 | output := fmt.Sprintf(`Creating directory %s. 46 | Writing out sample file %s 47 | Writing out sample file %s 48 | 49 | Almost done! Please add this line to the top of your main.js: 50 | 51 | require('cloud/app.js'); 52 | 53 | `, filepath.Join(h.Env.Root, "cloud", "views"), 54 | filepath.Join(h.Env.Root, "cloud", "app.js"), 55 | filepath.Join(h.Env.Root, "cloud", "views", "hello.ejs")) 56 | 57 | ensure.DeepEqual(t, h.Out.String(), output) 58 | ensure.DeepEqual(t, h.Err.String(), "") 59 | } 60 | 61 | func TestGenerateEjs(t *testing.T) { 62 | t.Parallel() 63 | h, g := newGenerateCmdHarness(t, expressEjs) 64 | defer h.Stop() 65 | g.run(h.Env) 66 | 67 | cloudRoot := filepath.Join(h.Env.Root, parsecli.CloudDir) 68 | 69 | appJs := filepath.Join(cloudRoot, "app.js") 70 | content, err := ioutil.ReadFile(appJs) 71 | ensure.Nil(t, err) 72 | ensure.DeepEqual(t, string(content), sampleAppJS) 73 | 74 | content, err = ioutil.ReadFile(filepath.Join(cloudRoot, "views", "hello.ejs")) 75 | ensure.Nil(t, err) 76 | ensure.DeepEqual(t, string(content), helloEJS) 77 | } 78 | 79 | func TestGenerateEjsExists(t *testing.T) { 80 | t.Parallel() 81 | h, g := newGenerateCmdHarness(t, expressEjs) 82 | defer h.Stop() 83 | g.run(h.Env) 84 | 85 | ensure.Err(t, g.run(h.Env), regexp.MustCompile("Please remove the above existing files and try again.")) 86 | } 87 | 88 | func TestGenerateJadeOutput(t *testing.T) { 89 | t.Parallel() 90 | h, g := newGenerateCmdHarness(t, expressJade) 91 | defer h.Stop() 92 | g.run(h.Env) 93 | 94 | output := fmt.Sprintf(`Creating directory %s. 95 | Writing out sample file %s 96 | Writing out sample file %s 97 | 98 | Almost done! Please add this line to the top of your main.js: 99 | 100 | require('cloud/app.js'); 101 | 102 | `, filepath.Join(h.Env.Root, "cloud", "views"), 103 | filepath.Join(h.Env.Root, "cloud", "app.js"), 104 | filepath.Join(h.Env.Root, "cloud", "views", "hello.jade")) 105 | 106 | ensure.DeepEqual(t, h.Out.String(), output) 107 | ensure.DeepEqual(t, h.Err.String(), "") 108 | } 109 | 110 | func TestGenerateJade(t *testing.T) { 111 | t.Parallel() 112 | h, g := newGenerateCmdHarness(t, expressJade) 113 | defer h.Stop() 114 | g.run(h.Env) 115 | 116 | cloudRoot := filepath.Join(h.Env.Root, parsecli.CloudDir) 117 | 118 | appJs := filepath.Join(cloudRoot, "app.js") 119 | content, err := ioutil.ReadFile(appJs) 120 | ensure.Nil(t, err) 121 | ensure.DeepEqual(t, string(content), strings.Replace(sampleAppJS, "ejs", "jade", -1)) 122 | 123 | content, err = ioutil.ReadFile(filepath.Join(cloudRoot, "views", "hello.jade")) 124 | ensure.Nil(t, err) 125 | ensure.DeepEqual(t, string(content), helloJade) 126 | } 127 | 128 | func TestGenerateJadeExists(t *testing.T) { 129 | t.Parallel() 130 | h, g := newGenerateCmdHarness(t, expressJade) 131 | defer h.Stop() 132 | g.run(h.Env) 133 | 134 | ensure.Err(t, g.run(h.Env), regexp.MustCompile("Please remove the above existing files and try again.")) 135 | } 136 | -------------------------------------------------------------------------------- /parsecmd/ios_symbol_uploader_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "regexp" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/ParsePlatform/parse-cli/parsecli" 11 | "github.com/facebookgo/ensure" 12 | ) 13 | 14 | var testDwarfPath = filepath.Join("Resources", "Test.xcarchive", 15 | "dSYMs", "Test.app.dSYM", "Contents", "Resources", "DWARF", "Test") 16 | 17 | func TestIOSSymbolUploaderValidate(t *testing.T) { 18 | t.Parallel() 19 | var i iosSymbolUploader 20 | err := i.validate() 21 | if runtime.GOOS != "darwin" { 22 | ensure.Err(t, err, 23 | regexp.MustCompile(`Upload of iOS symbol files is only available on OS X.`)) 24 | } else { 25 | ensure.Nil(t, err) 26 | } 27 | } 28 | 29 | func TestDwarfPath(t *testing.T) { 30 | t.Parallel() 31 | path, err := getDwarfPath("Resources/Test.xcarchive") 32 | ensure.Nil(t, err) 33 | ensure.DeepEqual(t, path, testDwarfPath) 34 | } 35 | 36 | func TestAcceptsPath(t *testing.T) { 37 | t.Parallel() 38 | i := iosSymbolUploader{Path: testDwarfPath} 39 | ensure.True(t, i.acceptsPath()) 40 | i.Path = "" 41 | ensure.False(t, i.acceptsPath()) 42 | } 43 | 44 | func TestBuildVersionFromXArchive(t *testing.T) { 45 | t.Parallel() 46 | var i iosSymbolUploader 47 | def, err := i.getBuildVersionFromXcarchive() 48 | ensure.DeepEqual(t, def, "1") 49 | ensure.Nil(t, err) 50 | 51 | i.Path = "./Resources/Test.xcarchive" 52 | v, err := i.getBuildVersionFromXcarchive() 53 | ensure.Nil(t, err) 54 | ensure.DeepEqual(t, v, "1.0") 55 | } 56 | 57 | func TestPrepareSymbolsFolder(t *testing.T) { 58 | t.Parallel() 59 | h := parsecli.NewHarness(t) 60 | defer h.Stop() 61 | 62 | h.MakeEmptyRoot() 63 | createRandomFiles(t, h) 64 | 65 | var i iosSymbolUploader 66 | ensure.Nil(t, i.prepareSymbolsFolder(h.Env.Root, h.Env)) 67 | files, err := readDirNames(h.Env.Root) 68 | ensure.Nil(t, err) 69 | ensure.DeepEqual(t, len(files), 0) 70 | } 71 | 72 | func TestSymbolConversionToolPath(t *testing.T) { 73 | t.Parallel() 74 | var i iosSymbolUploader 75 | h := parsecli.NewHarness(t) 76 | defer h.Stop() 77 | 78 | h.MakeEmptyRoot() 79 | ensure.Nil(t, os.MkdirAll(filepath.Join(h.Env.Root, ".parse"), 0755)) 80 | path, err := i.symbolConversionTool(h.Env.Root, 81 | func(path string, url string) error { 82 | return nil 83 | }, 84 | h.Env) 85 | ensure.Nil(t, err) 86 | ensure.DeepEqual(t, path, filepath.Join(h.Env.Root, ".parse", "parse_symbol_converter")) 87 | ensure.DeepEqual(t, h.Out.String(), 88 | `Additional resources installed. 89 | `) 90 | } 91 | -------------------------------------------------------------------------------- /parsecmd/jssdk.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "sort" 7 | 8 | "github.com/ParsePlatform/parse-cli/parsecli" 9 | "github.com/facebookgo/stackerr" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | type jsSDKVersion struct { 14 | JS []string `json:"js"` 15 | } 16 | 17 | type jsSDKCmd struct { 18 | all bool 19 | newVersion string 20 | } 21 | 22 | func (j *jsSDKCmd) getAllJSSdks(e *parsecli.Env) ([]string, error) { 23 | u := &url.URL{ 24 | Path: "jsVersions", 25 | } 26 | var response jsSDKVersion 27 | if _, err := e.ParseAPIClient.Get(u, &response); err != nil { 28 | return nil, stackerr.Wrap(err) 29 | } 30 | sort.Sort(naturalStrings(response.JS)) 31 | return response.JS, nil 32 | } 33 | 34 | func (j *jsSDKCmd) printVersions(e *parsecli.Env, c *parsecli.Context) error { 35 | allVersions, err := j.getAllJSSdks(e) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | currentVersion := c.Config.GetProjectConfig().Parse.JSSDK 41 | for _, version := range allVersions { 42 | prefix := " " 43 | if currentVersion == version { 44 | prefix = "* " 45 | } 46 | fmt.Fprintf(e.Out, "%s %s\n", prefix, version) 47 | } 48 | return nil 49 | } 50 | 51 | func (j *jsSDKCmd) getVersion(e *parsecli.Env, c *parsecli.Context) error { 52 | if c.Config.GetProjectConfig().Parse.JSSDK == "" { 53 | return stackerr.New("JavaScript SDK version not set for this project.") 54 | } 55 | fmt.Fprintf( 56 | e.Out, 57 | "Current JavaScript SDK version is %s\n", 58 | c.Config.GetProjectConfig().Parse.JSSDK, 59 | ) 60 | return nil 61 | } 62 | 63 | func (j *jsSDKCmd) setVersion(e *parsecli.Env, c *parsecli.Context) error { 64 | allVersions, err := j.getAllJSSdks(e) 65 | if err != nil { 66 | return err 67 | } 68 | valid := false 69 | for _, version := range allVersions { 70 | if version == j.newVersion { 71 | valid = true 72 | } 73 | } 74 | if !valid { 75 | return stackerr.New("Invalid SDK version selected.") 76 | } 77 | 78 | conf, err := parsecli.ConfigFromDir(e.Root) 79 | if err != nil { 80 | return err 81 | } 82 | conf.GetProjectConfig().Parse.JSSDK = j.newVersion 83 | if err := parsecli.StoreProjectConfig(e, conf); err != nil { 84 | return err 85 | } 86 | 87 | fmt.Fprintf(e.Out, "Current JavaScript SDK version is %s\n", conf.GetProjectConfig().Parse.JSSDK) 88 | return nil 89 | } 90 | 91 | // useLatestJSSDK is a utility method used by deploy & develop 92 | // to write set jssdk version to latest available, if none set 93 | func UseLatestJSSDK(e *parsecli.Env) error { 94 | var j jsSDKCmd 95 | versions, err := j.getAllJSSdks(e) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if len(versions) == 0 { 101 | return stackerr.New("No JavaScript SDK version is available.") 102 | } 103 | 104 | config, err := parsecli.ConfigFromDir(e.Root) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | config.GetProjectConfig().Parse.JSSDK = versions[0] 110 | return parsecli.StoreProjectConfig(e, config) 111 | } 112 | 113 | func (j *jsSDKCmd) run(e *parsecli.Env, c *parsecli.Context, args []string) error { 114 | switch { 115 | case j.all: 116 | return j.printVersions(e, c) 117 | default: 118 | if len(args) >= 1 { 119 | j.newVersion = args[0] 120 | return j.setVersion(e, c) 121 | } 122 | return j.getVersion(e, c) 123 | } 124 | } 125 | 126 | func NewJsSdkCmd(e *parsecli.Env) *cobra.Command { 127 | j := jsSDKCmd{} 128 | cmd := &cobra.Command{ 129 | Use: "jssdk [version]", 130 | Short: "Sets the Parse JavaScript SDK version to use in Cloud Code", 131 | Long: `Sets the Parse JavaScript SDK version to use in Cloud Code. ` + 132 | `Prints the version number currently set if none is specified.`, 133 | Run: parsecli.RunWithArgsClient(e, j.run), 134 | } 135 | cmd.Flags().BoolVarP(&j.all, "all", "a", j.all, 136 | "Shows all available JavaScript SDK version") 137 | return cmd 138 | } 139 | -------------------------------------------------------------------------------- /parsecmd/jssdk_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "regexp" 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 defaultParseConfig = &parsecli.ParseConfig{ 18 | ProjectConfig: &parsecli.ProjectConfig{ 19 | Type: parsecli.LegacyParseFormat, 20 | Parse: &parsecli.ParseProjectConfig{}, 21 | }, 22 | } 23 | 24 | func newJsSdkHarness(t testing.TB) *parsecli.Harness { 25 | h := parsecli.NewHarness(t) 26 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 27 | ensure.DeepEqual(t, r.URL.Path, "/1/jsVersions") 28 | rows := jsSDKVersion{JS: []string{"1.2.8", "1.2.9", "1.2.10", "1.2.11", "0.2.0"}} 29 | return &http.Response{ 30 | StatusCode: http.StatusOK, 31 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), 32 | }, nil 33 | }) 34 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 35 | return h 36 | } 37 | 38 | func newJsSdkHarnessError(t testing.TB) *parsecli.Harness { 39 | h := parsecli.NewHarness(t) 40 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 41 | ensure.DeepEqual(t, r.URL.Path, "/1/jsVersions") 42 | return &http.Response{ 43 | StatusCode: http.StatusExpectationFailed, 44 | Body: ioutil.NopCloser(strings.NewReader(`{"error":"something is wrong"}`)), 45 | }, nil 46 | }) 47 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 48 | return h 49 | } 50 | 51 | func newJsSdkHarnessWithConfig(t testing.TB) (*parsecli.Harness, *parsecli.Context) { 52 | h := newJsSdkHarness(t) 53 | h.MakeEmptyRoot() 54 | 55 | ensure.Nil(t, parsecli.CloneSampleCloudCode(h.Env, true)) 56 | h.Out.Reset() 57 | 58 | c, err := parsecli.ConfigFromDir(h.Env.Root) 59 | ensure.Nil(t, err) 60 | 61 | config, ok := (c).(*parsecli.ParseConfig) 62 | ensure.True(t, ok) 63 | 64 | return h, &parsecli.Context{Config: config} 65 | } 66 | 67 | func TestGetAllJSVersions(t *testing.T) { 68 | t.Parallel() 69 | h := newJsSdkHarness(t) 70 | defer h.Stop() 71 | j := jsSDKCmd{} 72 | versions, err := j.getAllJSSdks(h.Env) 73 | ensure.Nil(t, err) 74 | ensure.DeepEqual(t, versions, []string{"1.2.11", "1.2.10", "1.2.9", "1.2.8", "0.2.0"}) 75 | } 76 | 77 | func TestGetAllJSVersionsError(t *testing.T) { 78 | t.Parallel() 79 | h := newJsSdkHarnessError(t) 80 | defer h.Stop() 81 | j := jsSDKCmd{} 82 | _, err := j.getAllJSSdks(h.Env) 83 | ensure.Err(t, err, regexp.MustCompile(`something is wrong`)) 84 | } 85 | 86 | func TestPrintVersions(t *testing.T) { 87 | t.Parallel() 88 | h, c := newJsSdkHarnessWithConfig(t) 89 | defer h.Stop() 90 | j := jsSDKCmd{} 91 | ensure.Nil(t, j.printVersions(h.Env, c)) 92 | ensure.DeepEqual(t, h.Out.String(), 93 | ` 1.2.11 94 | 1.2.10 95 | 1.2.9 96 | 1.2.8 97 | 0.2.0 98 | `) 99 | } 100 | 101 | func TestSetVersionInvalid(t *testing.T) { 102 | t.Parallel() 103 | h, c := newJsSdkHarnessWithConfig(t) 104 | defer h.Stop() 105 | j := jsSDKCmd{newVersion: "1.2.12"} 106 | ensure.Err(t, j.setVersion(h.Env, c), regexp.MustCompile("Invalid SDK version selected")) 107 | } 108 | 109 | func TestSetVersionNoneSelected(t *testing.T) { 110 | t.Parallel() 111 | h := parsecli.NewHarness(t) 112 | defer h.Stop() 113 | 114 | c := &parsecli.Context{Config: defaultParseConfig} 115 | var j jsSDKCmd 116 | 117 | c.Config.GetProjectConfig().Parse.JSSDK = "1.2.1" 118 | ensure.Nil(t, j.getVersion(h.Env, c)) 119 | ensure.DeepEqual(t, "Current JavaScript SDK version is 1.2.1\n", 120 | h.Out.String()) 121 | 122 | c.Config.GetProjectConfig().Parse.JSSDK = "" 123 | ensure.Err(t, j.getVersion(h.Env, c), 124 | regexp.MustCompile("JavaScript SDK version not set for this project.")) 125 | } 126 | 127 | func TestSetValidVersion(t *testing.T) { 128 | t.Parallel() 129 | 130 | h, c := newJsSdkHarnessWithConfig(t) 131 | defer h.Stop() 132 | j := jsSDKCmd{newVersion: "1.2.11"} 133 | ensure.Nil(t, j.setVersion(h.Env, c)) 134 | ensure.DeepEqual(t, h.Out.String(), "Current JavaScript SDK version is 1.2.11\n") 135 | 136 | content, err := ioutil.ReadFile(filepath.Join(h.Env.Root, parsecli.ParseProject)) 137 | ensure.Nil(t, err) 138 | ensure.DeepEqual(t, string(content), `{ 139 | "project_type": 1, 140 | "parse": { 141 | "jssdk": "1.2.11" 142 | } 143 | }`) 144 | } 145 | 146 | // NOTE: testing for legacy config format 147 | func newLegacyJsSdkHarnessWithConfig(t testing.TB) (*parsecli.Harness, *parsecli.Context) { 148 | h := newJsSdkHarness(t) 149 | h.MakeEmptyRoot() 150 | 151 | ensure.Nil(t, os.Mkdir(filepath.Join(h.Env.Root, parsecli.ConfigDir), 0755)) 152 | path := filepath.Join(h.Env.Root, parsecli.LegacyConfigFile) 153 | ensure.Nil(t, ioutil.WriteFile(path, 154 | []byte(`{ 155 | "global": { 156 | "parseVersion" : "1.2.9" 157 | } 158 | }`), 159 | 0600)) 160 | 161 | c, err := parsecli.ConfigFromDir(h.Env.Root) 162 | ensure.Nil(t, err) 163 | 164 | config, ok := (c).(*parsecli.ParseConfig) 165 | ensure.True(t, ok) 166 | 167 | return h, &parsecli.Context{Config: config} 168 | } 169 | 170 | func TestLegacySetValidVersion(t *testing.T) { 171 | t.Parallel() 172 | 173 | h, c := newLegacyJsSdkHarnessWithConfig(t) 174 | defer h.Stop() 175 | j := jsSDKCmd{newVersion: "1.2.11"} 176 | ensure.Nil(t, j.setVersion(h.Env, c)) 177 | ensure.DeepEqual(t, h.Out.String(), "Current JavaScript SDK version is 1.2.11\n") 178 | } 179 | -------------------------------------------------------------------------------- /parsecmd/logs.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | "time" 9 | 10 | "github.com/ParsePlatform/parse-cli/parsecli" 11 | "github.com/facebookgo/stackerr" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | const logFollowSleepDuration = time.Second 16 | 17 | type parseTime struct { 18 | Type string `json:"__type"` 19 | ISO string `json:"iso"` 20 | } 21 | 22 | type logResponse struct { 23 | Timestamp parseTime `json:"timestamp"` 24 | Message string `json:"message"` 25 | } 26 | 27 | type logsCmd struct { 28 | num uint 29 | follow bool 30 | level string 31 | } 32 | 33 | func (l *logsCmd) run(e *parsecli.Env, c *parsecli.Context) error { 34 | level := strings.ToUpper(l.level) 35 | if level != "INFO" && level != "ERROR" { 36 | return stackerr.Newf("invalid level: %q", l.level) 37 | } 38 | l.level = level 39 | numIsSet := true 40 | if l.num == 0 { 41 | numIsSet = false 42 | l.num = 10 43 | } 44 | 45 | lastTime, err := l.round(e, c, nil) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if !l.follow { 51 | return nil 52 | } 53 | if !numIsSet { 54 | l.num = 100 // force num to 100 for follow 55 | } 56 | 57 | ticker := e.Clock.Ticker(logFollowSleepDuration) 58 | defer ticker.Stop() 59 | for range ticker.C { 60 | lastTime, err = l.round(e, c, lastTime) 61 | if err != nil { 62 | return err 63 | } 64 | } 65 | return nil 66 | } 67 | 68 | func (l *logsCmd) round(e *parsecli.Env, c *parsecli.Context, startTime *parseTime) (*parseTime, error) { 69 | v := make(url.Values) 70 | v.Set("n", fmt.Sprint(l.num)) 71 | v.Set("level", l.level) 72 | 73 | if startTime != nil { 74 | b, err := json.Marshal(startTime) 75 | if err != nil { 76 | return nil, stackerr.Wrap(err) 77 | } 78 | v.Set("startTime", string(b)) 79 | } 80 | 81 | u := &url.URL{ 82 | Path: "scriptlog", 83 | RawQuery: v.Encode(), 84 | } 85 | var rows []logResponse 86 | if _, err := e.ParseAPIClient.Get(u, &rows); err != nil { 87 | return nil, stackerr.Wrap(err) 88 | } 89 | // logs come back in reverse 90 | for i := len(rows) - 1; i >= 0; i-- { 91 | fmt.Fprintln(e.Out, rows[i].Message) 92 | } 93 | if len(rows) > 0 { 94 | return &rows[0].Timestamp, nil 95 | } 96 | return startTime, nil 97 | } 98 | 99 | func NewLogsCmd(e *parsecli.Env) *cobra.Command { 100 | l := logsCmd{ 101 | level: "INFO", 102 | } 103 | cmd := &cobra.Command{ 104 | Use: "logs", 105 | Short: "Prints out recent log messages", 106 | Long: "Prints out recent log messages.", 107 | Run: parsecli.RunWithClient(e, l.run), 108 | Aliases: []string{"log"}, 109 | } 110 | cmd.Flags().UintVarP(&l.num, "num", "n", l.num, 111 | "The number of the messages to display") 112 | cmd.Flags().BoolVarP(&l.follow, "follow", "f", l.follow, 113 | "Emulates tail -f and streams new messages from the server") 114 | cmd.Flags().StringVarP(&l.level, "level", "l", l.level, 115 | "The log level to restrict to. Can be 'INFO' or 'ERROR'.") 116 | return cmd 117 | } 118 | -------------------------------------------------------------------------------- /parsecmd/logs_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "regexp" 7 | "strings" 8 | "sync/atomic" 9 | "testing" 10 | 11 | "github.com/ParsePlatform/parse-cli/parsecli" 12 | "github.com/facebookgo/ensure" 13 | "github.com/facebookgo/parse" 14 | ) 15 | 16 | func TestLogInvalidLevel(t *testing.T) { 17 | t.Parallel() 18 | l := logsCmd{} 19 | h := parsecli.NewHarness(t) 20 | defer h.Stop() 21 | err := l.run(h.Env, nil) 22 | ensure.Err(t, err, regexp.MustCompile(`invalid level: ""`)) 23 | } 24 | 25 | func TestLogWithoutFollow(t *testing.T) { 26 | t.Parallel() 27 | l := logsCmd{level: "INFO"} 28 | h := parsecli.NewHarness(t) 29 | defer h.Stop() 30 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 31 | ensure.DeepEqual(t, r.URL.Path, "/1/scriptlog") 32 | rows := []logResponse{{Message: "foo bar"}, {Message: "baz"}} 33 | return &http.Response{ 34 | StatusCode: http.StatusOK, 35 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), 36 | }, nil 37 | }) 38 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 39 | err := l.run(h.Env, &parsecli.Context{}) 40 | ensure.Nil(t, err) 41 | ensure.DeepEqual(t, h.Out.String(), "baz\nfoo bar\n") 42 | } 43 | 44 | func TestLogWithFollow(t *testing.T) { 45 | t.Parallel() 46 | l := logsCmd{level: "INFO", follow: true} 47 | h := parsecli.NewHarness(t) 48 | defer h.Stop() 49 | var round int64 50 | round1Time := parseTime{ISO: "iso1", Type: "type1"} 51 | round1TimeStr := jsonStr(t, round1Time) 52 | round3Time := parseTime{ISO: "iso2", Type: "type2"} 53 | round3TimeStr := jsonStr(t, round3Time) 54 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 55 | switch atomic.AddInt64(&round, 1) { 56 | case 1: 57 | // expect no timestamp, return some data 58 | ensure.DeepEqual(t, r.FormValue("startTime"), "") 59 | rows := []logResponse{{Message: "foo bar", Timestamp: round1Time}} 60 | return &http.Response{ 61 | StatusCode: http.StatusOK, 62 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), 63 | }, nil 64 | case 2: 65 | // expect the timestamp from case 1, return no new data 66 | ensure.DeepEqual(t, r.FormValue("startTime"), round1TimeStr) 67 | rows := []logResponse{} 68 | return &http.Response{ 69 | StatusCode: http.StatusOK, 70 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), 71 | }, nil 72 | case 3: 73 | // expect the timestamp from case 1, return some new data 74 | ensure.DeepEqual(t, r.FormValue("startTime"), round1TimeStr) 75 | rows := []logResponse{{Message: "baz", Timestamp: round3Time}} 76 | return &http.Response{ 77 | StatusCode: http.StatusOK, 78 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), 79 | }, nil 80 | case 4: 81 | // expect the timestamp from case 3, return error 82 | ensure.DeepEqual(t, r.FormValue("startTime"), round3TimeStr) 83 | return &http.Response{ 84 | StatusCode: http.StatusInternalServerError, 85 | Status: http.StatusText(http.StatusInternalServerError), 86 | Body: ioutil.NopCloser(strings.NewReader("a")), 87 | }, nil 88 | } 89 | panic("unexpected request") 90 | }) 91 | 92 | stop := make(chan struct{}) 93 | go func() { 94 | for { 95 | select { 96 | case <-stop: 97 | return 98 | default: 99 | h.Clock.Add(logFollowSleepDuration) 100 | } 101 | } 102 | }() 103 | 104 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 105 | err := l.run(h.Env, &parsecli.Context{}) 106 | close(stop) 107 | 108 | ensure.Err(t, err, regexp.MustCompile(`parse: error with status=500 and body="a"`)) 109 | ensure.DeepEqual(t, h.Out.String(), "foo bar\nbaz\n") 110 | ensure.DeepEqual(t, h.Err.String(), "") 111 | ensure.DeepEqual(t, atomic.LoadInt64(&round), int64(4)) 112 | } 113 | -------------------------------------------------------------------------------- /parsecmd/new.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ParsePlatform/parse-cli/parsecli" 7 | "github.com/facebookgo/parse" 8 | ) 9 | 10 | func CloneSampleCloudCode( 11 | e *parsecli.Env, 12 | isNew, configOnly bool, 13 | appConfig parsecli.AppConfig) (bool, error) { 14 | dumpTemplate := false 15 | if !isNew && !configOnly { 16 | // if parse app was already created try to fetch Cloud Code and populate dir 17 | masterKey, err := appConfig.GetMasterKey(e) 18 | if err != nil { 19 | return false, err 20 | } 21 | e.ParseAPIClient = e.ParseAPIClient.WithCredentials( 22 | parse.MasterKey{ 23 | ApplicationID: appConfig.GetApplicationID(), 24 | MasterKey: masterKey, 25 | }, 26 | ) 27 | 28 | d := &downloadCmd{destination: e.Root} 29 | err = d.run(e, nil) 30 | if err != nil { 31 | if err == errNoFiles { 32 | dumpTemplate = true 33 | } else { 34 | fmt.Fprintln( 35 | e.Out, 36 | ` 37 | NOTE: If you like to fetch the latest deployed Cloud Code from Parse, 38 | you can use the "parse download" command after finishing the set up. 39 | This will download Cloud Code to a temporary location. 40 | `, 41 | ) 42 | } 43 | } 44 | } 45 | dumpTemplate = (isNew || dumpTemplate) && !configOnly 46 | return dumpTemplate, parsecli.CloneSampleCloudCode(e, dumpTemplate) 47 | } 48 | -------------------------------------------------------------------------------- /parsecmd/parse_symbol_uploader.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "bufio" 5 | "crypto/md5" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "mime" 10 | "net/http" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | 15 | "github.com/ParsePlatform/parse-cli/parsecli" 16 | "github.com/facebookgo/errgroup" 17 | "github.com/facebookgo/stackerr" 18 | ) 19 | 20 | func base64MD5OfFile(filename string) (string, error) { 21 | file, err := os.Open(filename) 22 | if err != nil { 23 | return "", stackerr.Wrap(err) 24 | } 25 | defer file.Close() 26 | 27 | h := md5.New() 28 | if _, err := io.Copy(h, file); err != nil { 29 | return "", stackerr.Wrap(err) 30 | } 31 | if err := file.Close(); err != nil { 32 | return "", stackerr.Wrap(err) 33 | } 34 | return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil 35 | } 36 | 37 | func uploadSymbolFiles(files []string, commonHeaders map[string]string, removeFiles bool, e *parsecli.Env) error { 38 | var wg errgroup.Group 39 | uploadFile := func(filename string, e *parsecli.Env) { 40 | defer wg.Done() 41 | name := filepath.Base(filepath.Clean(filename)) 42 | file, err := os.Open(filename) 43 | if err != nil { 44 | wg.Error(stackerr.Wrap(err)) 45 | return 46 | } 47 | defer file.Close() 48 | 49 | req, err := http.NewRequest("POST", path.Join("symbolFiles", name), bufio.NewReader(file)) 50 | if err != nil { 51 | wg.Error(stackerr.Wrap(err)) 52 | return 53 | } 54 | 55 | if req.Header == nil { 56 | req.Header = make(http.Header) 57 | } 58 | for key, val := range commonHeaders { 59 | req.Header.Add(key, val) 60 | } 61 | hash, err := base64MD5OfFile(filename) 62 | if err != nil { 63 | wg.Error(err) 64 | return 65 | } 66 | req.Header.Add("Content-MD5", hash) 67 | mimeType := mime.TypeByExtension(filepath.Ext(name)) 68 | if mimeType == "" { 69 | mimeType = "application/octet-stream" 70 | } 71 | req.Header.Add("Content-Type", mimeType) 72 | res := make(map[string]interface{}) 73 | if _, err := e.ParseAPIClient.Do(req, nil, &res); err != nil { 74 | wg.Error(err) 75 | return 76 | } 77 | if removeFiles { 78 | if err := file.Close(); err != nil { 79 | wg.Error(err) 80 | return 81 | } 82 | if err := os.Remove(filename); err != nil { 83 | wg.Error(err) 84 | return 85 | } 86 | } 87 | } 88 | for _, file := range files { 89 | wg.Add(1) 90 | go uploadFile(file, e) 91 | } 92 | err := wg.Wait() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | fmt.Fprintln(e.Out, "Uploaded symbol files.") 98 | return nil 99 | } 100 | 101 | func readDirNames(dirname string) ([]string, error) { 102 | var files []string 103 | err := filepath.Walk(dirname, func(path string, 104 | info os.FileInfo, 105 | err error) error { 106 | if path == dirname { 107 | return nil 108 | } 109 | files = append(files, path) 110 | if info.IsDir() { 111 | return filepath.SkipDir 112 | } 113 | return nil 114 | }) 115 | if err != nil { 116 | return nil, stackerr.Wrap(err) 117 | } 118 | return files, nil 119 | } 120 | -------------------------------------------------------------------------------- /parsecmd/parse_symbol_uploader_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "os" 8 | "path/filepath" 9 | "sort" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/ParsePlatform/parse-cli/parsecli" 14 | "github.com/facebookgo/ensure" 15 | "github.com/facebookgo/parse" 16 | ) 17 | 18 | func createRandomFiles(t *testing.T, h *parsecli.Harness) { 19 | for _, filename := range []string{"a", "b"} { 20 | ensure.Nil(t, ioutil.WriteFile(filepath.Join(h.Env.Root, filename), 21 | []byte(fmt.Sprintf(`Content of file : %s`, filename)), 0755)) 22 | } 23 | } 24 | 25 | func TestBase64MD5OfFile(t *testing.T) { 26 | t.Parallel() 27 | h := parsecli.NewHarness(t) 28 | defer h.Stop() 29 | 30 | h.MakeEmptyRoot() 31 | testFile := filepath.Join(h.Env.Root, "test") 32 | ensure.Nil(t, ioutil.WriteFile(testFile, 33 | []byte(`A test string`), 0755)) 34 | 35 | val, err := base64MD5OfFile(testFile) 36 | ensure.Nil(t, err) 37 | ensure.DeepEqual(t, val, "mc5NvcbbGrh2RDETriS4Fg==") 38 | } 39 | 40 | func TestBase64MD5OfFileNoFile(t *testing.T) { 41 | t.Parallel() 42 | h := parsecli.NewHarness(t) 43 | defer h.Stop() 44 | 45 | h.MakeEmptyRoot() 46 | testFile := filepath.Join(h.Env.Root, "test") 47 | _, err := base64MD5OfFile(testFile) 48 | ensure.NotNil(t, err) 49 | } 50 | 51 | func TestReadDirNames(t *testing.T) { 52 | t.Parallel() 53 | h := parsecli.NewHarness(t) 54 | defer h.Stop() 55 | 56 | h.MakeEmptyRoot() 57 | createRandomFiles(t, h) 58 | files, err := readDirNames(h.Env.Root) 59 | sort.Strings(files) 60 | ensure.DeepEqual(t, len(files), 2) 61 | ensure.DeepEqual(t, files[:], []string{ 62 | filepath.Join(h.Env.Root, "a"), 63 | filepath.Join(h.Env.Root, "b"), 64 | }) 65 | ensure.Nil(t, err) 66 | } 67 | 68 | func TestUploadFiles(t *testing.T) { 69 | t.Parallel() 70 | h := parsecli.NewHarness(t) 71 | defer h.Stop() 72 | 73 | h.MakeEmptyRoot() 74 | createRandomFiles(t, h) 75 | 76 | names := []string{"a", "b"} 77 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 78 | switch filepath.Base(r.URL.Path) { 79 | case names[0]: 80 | ensure.NotNil(t, r.Header) 81 | ensure.DeepEqual(t, r.Header.Get("Key"), "Value") 82 | ensure.DeepEqual(t, r.Header.Get("Content-Type"), "application/octet-stream") 83 | ensure.DeepEqual(t, r.Header.Get("Content-MD5"), "4JnleFGzGppuArF6N50EWg==") 84 | return &http.Response{ 85 | StatusCode: http.StatusOK, 86 | Body: ioutil.NopCloser(strings.NewReader(`{"status":"success"}`)), 87 | }, nil 88 | case names[1]: 89 | ensure.NotNil(t, r.Header) 90 | ensure.DeepEqual(t, r.Header.Get("Key"), "Value") 91 | ensure.DeepEqual(t, r.Header.Get("Content-Type"), "application/octet-stream") 92 | ensure.DeepEqual(t, r.Header.Get("Content-MD5"), "Fv43qsp6mnGCJlC00VkOcA==") 93 | return &http.Response{ 94 | StatusCode: http.StatusOK, 95 | Body: ioutil.NopCloser(strings.NewReader(`{"status":"success"}`)), 96 | }, nil 97 | default: 98 | return &http.Response{ 99 | StatusCode: http.StatusInternalServerError, 100 | Body: ioutil.NopCloser(strings.NewReader(`{"error":"something is wrong"}`)), 101 | }, nil 102 | } 103 | }) 104 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 105 | 106 | var filenames []string 107 | for _, name := range names { 108 | filenames = append(filenames, filepath.Join(h.Env.Root, name)) 109 | } 110 | ensure.Nil(t, uploadSymbolFiles(filenames[:], 111 | map[string]string{"Key": "Value"}, true, h.Env)) 112 | for _, filename := range filenames { 113 | _, err := os.Lstat(filename) 114 | ensure.NotNil(t, err) 115 | ensure.True(t, os.IsNotExist(err)) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /parsecmd/parseignore.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/ParsePlatform/parse-cli/parsecli" 10 | "github.com/facebookgo/parseignore" 11 | "github.com/facebookgo/symwalk" 12 | ) 13 | 14 | func ignoreErrors(errors []error, e *parsecli.Env) string { 15 | l := len(errors) 16 | 17 | var b bytes.Buffer 18 | for n, err := range errors { 19 | b.WriteString(parsecli.ErrorString(e, err)) 20 | if n != l-1 { 21 | b.WriteString("\n") 22 | } 23 | } 24 | return b.String() 25 | } 26 | 27 | type legacyRulesMatcher struct{} 28 | 29 | func (l legacyRulesMatcher) Match(path string, fi os.FileInfo) (parseignore.Decision, error) { 30 | parts := strings.Split(path, string(filepath.Separator)) 31 | for _, part := range parts { 32 | if strings.HasPrefix(part, ".") || 33 | strings.HasPrefix(part, "#") || 34 | strings.HasSuffix(part, ".swp") || 35 | strings.HasSuffix(part, "~") { 36 | return parseignore.Exclude, nil 37 | } 38 | } 39 | return parseignore.Pass, nil 40 | } 41 | 42 | func parseIgnoreMatcher(content []byte) (parseignore.Matcher, []error) { 43 | if content != nil { 44 | matcher, errors := parseignore.CompilePatterns(content) 45 | return parseignore.MultiMatcher(matcher, legacyRulesMatcher{}), errors 46 | } 47 | return legacyRulesMatcher{}, nil 48 | } 49 | 50 | // parseIgnoreWalk is a helper function user by the Parse CLI. It walks the given 51 | // root path (traversing symbolic links if any) and calls walkFn only 52 | // on files that were not ignored by the given Matcher. 53 | // Note: It ignores any errors encountered during matching 54 | func parseIgnoreWalk(matcher parseignore.Matcher, 55 | root string, 56 | walkFn filepath.WalkFunc) ([]error, error) { 57 | var errors []error 58 | ignoresWalkFn := func(path string, info os.FileInfo, err error) error { 59 | // if root==path relPath="." which is ignored by legacyRule 60 | // hence the special handling of root 61 | if err != nil || root == path { 62 | return walkFn(path, info, err) 63 | } 64 | 65 | relPath, err := filepath.Rel(root, path) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | exclude, err := matcher.Match(relPath, info) 71 | if err != nil { 72 | errors = append(errors, err) 73 | return nil 74 | } 75 | 76 | if exclude == parseignore.Exclude { 77 | if info.IsDir() { 78 | return filepath.SkipDir 79 | } 80 | return nil 81 | } 82 | 83 | return walkFn(path, info, err) 84 | } 85 | 86 | return errors, symwalk.Walk(root, ignoresWalkFn) 87 | } 88 | -------------------------------------------------------------------------------- /parsecmd/parseignore_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/facebookgo/ensure" 12 | "github.com/facebookgo/testname" 13 | ) 14 | 15 | func makeEmptyRoot(t *testing.T) string { 16 | prefix := fmt.Sprintf("%s-", testname.Get("parse-cli-")) 17 | root, err := ioutil.TempDir("", prefix) 18 | ensure.Nil(t, err) 19 | return root 20 | } 21 | 22 | func initProject(t *testing.T, root string) ([]string, []string) { 23 | dirs := []string{ 24 | filepath.Join(root, "tester"), 25 | filepath.Join(root, "tester", "inside"), 26 | filepath.Join(root, "tester", "inside", "test"), 27 | filepath.Join(root, "tester", "inside", "tester"), 28 | } 29 | 30 | for _, dir := range dirs { 31 | ensure.Nil(t, os.MkdirAll(dir, 0755)) 32 | } 33 | 34 | files := []string{ 35 | filepath.Join(root, "tester", "test"), 36 | filepath.Join(root, "tester", "inside", "tester", "test"), 37 | } 38 | 39 | for _, file := range files { 40 | f, err := os.Create(file) 41 | ensure.Nil(t, err) 42 | defer f.Close() 43 | } 44 | 45 | return files, dirs 46 | } 47 | 48 | func TestPatternWalker(t *testing.T) { 49 | t.Parallel() 50 | 51 | root := makeEmptyRoot(t) 52 | defer os.RemoveAll(root) 53 | 54 | files, dirs := initProject(t, root) 55 | dirs = append(dirs, root) 56 | 57 | var visitedFiles, visitedDirs []string 58 | walkFn := func(path string, info os.FileInfo, err error) error { 59 | if err != nil { 60 | return err 61 | } 62 | if info.IsDir() { 63 | visitedDirs = append(visitedDirs, path) 64 | return nil 65 | } 66 | visitedFiles = append(visitedFiles, path) 67 | return nil 68 | } 69 | 70 | testCases := []struct { 71 | ignores string 72 | expectedFiles, expectedDirs []string 73 | }{ 74 | { 75 | ` 76 | test/ 77 | !tester/test 78 | `, 79 | []string{}, 80 | []string{ 81 | filepath.Join(root, "tester", "inside", "test"), 82 | }, 83 | }, 84 | { 85 | ` 86 | test 87 | !tester/test 88 | `, 89 | []string{ 90 | filepath.Join(root, "tester", "inside", "tester", "test"), 91 | }, 92 | []string{ 93 | filepath.Join(root, "tester", "inside", "test"), 94 | }, 95 | }, 96 | { 97 | ` 98 | test* 99 | `, 100 | files, 101 | []string{ 102 | filepath.Join(root, "tester"), 103 | filepath.Join(root, "tester", "inside"), 104 | filepath.Join(root, "tester", "inside", "test"), 105 | filepath.Join(root, "tester", "inside", "tester"), 106 | }, 107 | }, 108 | } 109 | 110 | for _, testCase := range testCases { 111 | matcher, errors := parseIgnoreMatcher([]byte(testCase.ignores)) 112 | ensure.DeepEqual(t, len(errors), 0) 113 | 114 | visitedFiles, visitedDirs = nil, nil 115 | errors, err := parseIgnoreWalk(matcher, root, walkFn) 116 | ensure.Nil(t, err) 117 | ensure.DeepEqual(t, len(errors), 0) 118 | 119 | var aggFiles, aggDirs []string 120 | aggFiles = append(aggFiles, testCase.expectedFiles...) 121 | aggFiles = append(aggFiles, visitedFiles...) 122 | 123 | aggDirs = append(aggDirs, testCase.expectedDirs...) 124 | aggDirs = append(aggDirs, visitedDirs...) 125 | 126 | ensure.SameElements(t, aggFiles, files) 127 | ensure.SameElements(t, aggDirs, dirs) 128 | } 129 | } 130 | 131 | func TestPatternWalkerSymLink(t *testing.T) { 132 | t.Parallel() 133 | 134 | root := makeEmptyRoot(t) 135 | defer os.RemoveAll(root) 136 | 137 | files, dirs := initProject(t, root) 138 | ensure.Nil(t, os.Symlink(filepath.Join(root, "tester"), filepath.Join(root, "link"))) 139 | 140 | for i := 0; i < len(files); i++ { 141 | files[i] = strings.Replace(files[i], "tester", "link", 1) 142 | } 143 | for i := 0; i < len(dirs); i++ { 144 | dirs[i] = strings.Replace(dirs[i], "tester", "link", 1) 145 | } 146 | 147 | var visitedFiles, visitedDirs []string 148 | walkFn := func(path string, info os.FileInfo, err error) error { 149 | if err != nil { 150 | return err 151 | } 152 | if info.IsDir() { 153 | visitedDirs = append(visitedDirs, path) 154 | return nil 155 | } 156 | visitedFiles = append(visitedFiles, path) 157 | return nil 158 | } 159 | 160 | testCases := []struct { 161 | ignores string 162 | expectedFiles, expectedDirs []string 163 | }{ 164 | { 165 | ` 166 | test/ 167 | !tester/test 168 | `, 169 | []string{}, 170 | []string{ 171 | filepath.Join(root, "link", "inside", "test"), 172 | }, 173 | }, 174 | { 175 | ` 176 | test 177 | !tester/test 178 | `, 179 | []string{ 180 | filepath.Join(root, "link", "inside", "tester", "test"), 181 | filepath.Join(root, "link", "test"), 182 | }, 183 | []string{ 184 | filepath.Join(root, "link", "inside", "test"), 185 | }, 186 | }, 187 | { 188 | ` 189 | test* 190 | `, 191 | []string{ 192 | filepath.Join(root, "link", "test"), 193 | filepath.Join(root, "link", "inside", "tester", "test"), 194 | }, 195 | []string{ 196 | filepath.Join(root, "link", "inside", "test"), 197 | filepath.Join(root, "link", "inside", "tester"), 198 | }, 199 | }, 200 | } 201 | 202 | for _, testCase := range testCases { 203 | matcher, errs := parseIgnoreMatcher([]byte(testCase.ignores)) 204 | ensure.DeepEqual(t, len(errs), 0) 205 | 206 | visitedFiles, visitedDirs = nil, nil 207 | errors, err := parseIgnoreWalk(matcher, filepath.Join(root, "link"), walkFn) 208 | ensure.Nil(t, err) 209 | ensure.DeepEqual(t, len(errors), 0) 210 | 211 | var aggFiles, aggDirs []string 212 | aggFiles = append(aggFiles, testCase.expectedFiles...) 213 | aggFiles = append(aggFiles, visitedFiles...) 214 | 215 | aggDirs = append(aggDirs, testCase.expectedDirs...) 216 | aggDirs = append(aggDirs, visitedDirs...) 217 | 218 | ensure.SameElements(t, aggFiles, files) 219 | ensure.SameElements(t, aggDirs, dirs) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /parsecmd/releases.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | "sort" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "github.com/ParsePlatform/parse-cli/parsecli" 12 | "github.com/facebookgo/stackerr" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | type userFiles struct { 17 | Cloud map[string]interface{} `json:"cloud"` 18 | Public map[string]interface{} `json:"public"` 19 | } 20 | 21 | type releasesResponse struct { 22 | Version string `json:"version"` 23 | Description string `json:"description"` 24 | Timestamp string `json:"timestamp"` 25 | UserFiles string `json:"userFiles"` 26 | } 27 | 28 | type releasesCmd struct { 29 | version string 30 | } 31 | 32 | func (r *releasesCmd) printFileNames( 33 | fileVersions map[string]interface{}, 34 | e *parsecli.Env) { 35 | var files []string 36 | for name := range fileVersions { 37 | files = append(files, name) 38 | } 39 | sort.Strings(files) 40 | fmt.Fprintln(e.Out, strings.Join(files, "\n")) 41 | } 42 | 43 | func (r *releasesCmd) printFiles(version string, 44 | releases []releasesResponse, 45 | e *parsecli.Env) error { 46 | var files string 47 | for _, release := range releases { 48 | if release.Version == version { 49 | files = release.UserFiles 50 | break 51 | } 52 | } 53 | if files == "" { 54 | return stackerr.Newf(`Unable to fetch files for release version: %s 55 | Note that you can list files for all releases shown in "parse releases"`, 56 | version) 57 | } 58 | var versionFileNames userFiles 59 | if err := json.NewDecoder( 60 | strings.NewReader(files), 61 | ).Decode(&versionFileNames); err != nil { 62 | return stackerr.Wrap(err) 63 | } 64 | if len(versionFileNames.Cloud) != 0 { 65 | fmt.Fprintf(e.Out, "Deployed Cloud Code files:\n") 66 | r.printFileNames(versionFileNames.Cloud, e) 67 | } 68 | if len(versionFileNames.Cloud) != 0 && len(versionFileNames.Public) != 0 { 69 | fmt.Fprintln(e.Out) 70 | } 71 | if len(versionFileNames.Public) != 0 { 72 | fmt.Fprintf(e.Out, "Deployed public hosting files:\n") 73 | r.printFileNames(versionFileNames.Public, e) 74 | } 75 | return nil 76 | } 77 | 78 | func (r *releasesCmd) run(e *parsecli.Env, c *parsecli.Context) error { 79 | u := &url.URL{ 80 | Path: "releases", 81 | } 82 | var releasesList []releasesResponse 83 | if _, err := e.ParseAPIClient.Get(u, &releasesList); err != nil { 84 | return stackerr.Wrap(err) 85 | } 86 | 87 | if r.version != "" { 88 | return r.printFiles(r.version, releasesList, e) 89 | } 90 | 91 | w := new(tabwriter.Writer) 92 | w.Init(e.Out, 32, 8, 0, ' ', 0) 93 | fmt.Fprintln(w, "Name\tDescription\tDate") 94 | for _, release := range releasesList { 95 | description := "No release notes given" 96 | if release.Description != "" { 97 | description = release.Description 98 | } 99 | fmt.Fprintf(w, "%s\t%s\t%s\n", release.Version, description, release.Timestamp) 100 | } 101 | w.Flush() 102 | return nil 103 | } 104 | 105 | func NewReleasesCmd(e *parsecli.Env) *cobra.Command { 106 | r := &releasesCmd{} 107 | cmd := &cobra.Command{ 108 | Use: "releases [app]", 109 | Short: "Gets the releases for a Parse App", 110 | Long: "Prints the releases the server knows about.", 111 | Run: parsecli.RunWithClient(e, r.run), 112 | } 113 | cmd.Flags().StringVarP(&r.version, "version", "v", r.version, 114 | "List files names of the deployed version.") 115 | return cmd 116 | } 117 | -------------------------------------------------------------------------------- /parsecmd/releases_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "regexp" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/ParsePlatform/parse-cli/parsecli" 11 | "github.com/facebookgo/ensure" 12 | "github.com/facebookgo/parse" 13 | "github.com/facebookgo/stackerr" 14 | ) 15 | 16 | func newReleasesCmdHarness(t testing.TB) (*parsecli.Harness, *releasesCmd) { 17 | h := parsecli.NewHarness(t) 18 | h.MakeEmptyRoot() 19 | r := &releasesCmd{} 20 | return h, r 21 | } 22 | 23 | func TestReleasesCmd(t *testing.T) { 24 | h, r := newReleasesCmdHarness(t) 25 | defer h.Stop() 26 | rows := []releasesResponse{ 27 | {Version: "v1", Description: "version 1", Timestamp: "time 1"}, 28 | {Version: "v2", Description: "version 2", Timestamp: "time 2"}, 29 | } 30 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 31 | ensure.DeepEqual(t, r.URL.Path, "/1/releases") 32 | return &http.Response{ 33 | StatusCode: http.StatusOK, 34 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, rows))), 35 | }, nil 36 | }) 37 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 38 | 39 | ensure.Nil(t, r.run(h.Env, &parsecli.Context{})) 40 | 41 | expected := `Name Description Date 42 | v1 version 1 time 1 43 | v2 version 2 time 2 44 | ` 45 | ensure.DeepEqual(t, h.Out.String(), expected) 46 | } 47 | 48 | func TestReleasesCmdError(t *testing.T) { 49 | h, c := newReleasesCmdHarness(t) 50 | defer h.Stop() 51 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 52 | return nil, stackerr.New("Throws error") 53 | }) 54 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 55 | 56 | ensure.NotNil(t, c.run(h.Env, &parsecli.Context{})) 57 | } 58 | 59 | func TestReleasesCmdPrintVersion(t *testing.T) { 60 | h, r := newReleasesCmdHarness(t) 61 | releases := []releasesResponse{ 62 | {Version: "v1", 63 | UserFiles: `{ 64 | "cloud": {"main.js": "1", "app.js": "1", "views/index.js": "1"} 65 | }`, 66 | }, 67 | {Version: "v2", 68 | UserFiles: `{ 69 | "cloud": {"main.js": "2", "app.js": "2", "views/docs.js": "1"}, 70 | "public": {"index.html": "2", "docs.html": "2"} 71 | }`, 72 | }, 73 | {Version: "v2", 74 | UserFiles: `{ 75 | "cloud": {"v2_main.js": "2", "v2_app.js": "2", "views/v2_docs.js": "2"}, 76 | "public": {"v2_index.html": "2", "v2_docs.html": "2"} 77 | }`, 78 | }, 79 | } 80 | err := r.printFiles("", releases, h.Env) 81 | ensure.Err(t, err, regexp.MustCompile("Unable to fetch files for release version")) 82 | 83 | h.Out.Reset() 84 | err = r.printFiles("v1", releases, h.Env) 85 | ensure.Nil(t, err) 86 | ensure.DeepEqual(t, h.Out.String(), `Deployed Cloud Code files: 87 | app.js 88 | main.js 89 | views/index.js 90 | `) 91 | 92 | h.Out.Reset() 93 | err = r.printFiles("v2", releases, h.Env) 94 | ensure.Nil(t, err) 95 | ensure.DeepEqual(t, h.Out.String(), `Deployed Cloud Code files: 96 | app.js 97 | main.js 98 | views/docs.js 99 | 100 | Deployed public hosting files: 101 | docs.html 102 | index.html 103 | `) 104 | } 105 | -------------------------------------------------------------------------------- /parsecmd/rollback.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/ParsePlatform/parse-cli/parsecli" 8 | "github.com/facebookgo/stackerr" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | type rollbackCmd struct { 13 | ReleaseName string 14 | } 15 | 16 | type rollbackInfo struct { 17 | ReleaseName string `json:"releaseName,omitEmpty"` 18 | } 19 | 20 | func (r *rollbackCmd) run(e *parsecli.Env, c *parsecli.Context) error { 21 | var req rollbackInfo 22 | message := "previous release" 23 | if r.ReleaseName != "" { 24 | message = r.ReleaseName 25 | req.ReleaseName = r.ReleaseName 26 | } 27 | 28 | fmt.Fprintf(e.Out, "Rolling back to %s\n", message) 29 | 30 | var response rollbackInfo 31 | if _, err := e.ParseAPIClient.Post(&url.URL{Path: "deploy"}, 32 | &req, &response); err != nil { 33 | return stackerr.Newf("Rollback failed with %s", stackerr.Wrap(err)) 34 | } 35 | fmt.Fprintf(e.Out, "Rolled back to version %s\n", response.ReleaseName) 36 | return nil 37 | } 38 | 39 | func NewRollbackCmd(e *parsecli.Env) *cobra.Command { 40 | r := rollbackCmd{} 41 | cmd := &cobra.Command{ 42 | Use: "rollback [app]", 43 | Short: "Rolls back the version for the given app", 44 | Long: "Rolls back the version for the given app.", 45 | Run: parsecli.RunWithClient(e, r.run), 46 | } 47 | cmd.Flags().StringVarP(&r.ReleaseName, "release", "r", r.ReleaseName, 48 | "Provides an optional release to rollback to. If no release is provided, rolls back to the previous release.") 49 | return cmd 50 | } 51 | -------------------------------------------------------------------------------- /parsecmd/rollback_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "regexp" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/ParsePlatform/parse-cli/parsecli" 12 | "github.com/facebookgo/ensure" 13 | "github.com/facebookgo/parse" 14 | ) 15 | 16 | func newRollbackCmdHarness(t testing.TB) *parsecli.Harness { 17 | h := parsecli.NewHarness(t) 18 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 19 | ensure.DeepEqual(t, r.URL.Path, "/1/deploy") 20 | var req, res rollbackInfo 21 | ensure.Nil(t, json.NewDecoder(r.Body).Decode(&req)) 22 | if req.ReleaseName == "" { 23 | res.ReleaseName = "v0" 24 | } else { 25 | res.ReleaseName = req.ReleaseName 26 | } 27 | return &http.Response{ 28 | StatusCode: http.StatusOK, 29 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), 30 | }, nil 31 | }) 32 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 33 | return h 34 | } 35 | 36 | func TestRollbackToPrevious(t *testing.T) { 37 | t.Parallel() 38 | var r rollbackCmd 39 | h := newRollbackCmdHarness(t) 40 | defer h.Stop() 41 | ensure.Nil(t, r.run(h.Env, nil)) 42 | ensure.DeepEqual(t, h.Out.String(), 43 | `Rolling back to previous release 44 | Rolled back to version v0 45 | `) 46 | } 47 | 48 | func TestRollbackToVersionOne(t *testing.T) { 49 | t.Parallel() 50 | r := rollbackCmd{ReleaseName: "v1"} 51 | h := newRollbackCmdHarness(t) 52 | defer h.Stop() 53 | ensure.Nil(t, r.run(h.Env, nil)) 54 | ensure.DeepEqual(t, h.Out.String(), 55 | `Rolling back to v1 56 | Rolled back to version v1 57 | `) 58 | } 59 | 60 | func TestRollbackError(t *testing.T) { 61 | t.Parallel() 62 | var r rollbackCmd 63 | h := parsecli.NewHarness(t) 64 | defer h.Stop() 65 | var res struct{ Error string } 66 | res.Error = "something is wrong" 67 | ht := parsecli.TransportFunc(func(r *http.Request) (*http.Response, error) { 68 | ensure.DeepEqual(t, r.URL.Path, "/1/deploy") 69 | return &http.Response{ 70 | StatusCode: http.StatusExpectationFailed, 71 | Body: ioutil.NopCloser(strings.NewReader(jsonStr(t, &res))), 72 | }, nil 73 | }) 74 | h.Env.ParseAPIClient = &parsecli.ParseAPIClient{APIClient: &parse.Client{Transport: ht}} 75 | ensure.Err(t, r.run(h.Env, nil), regexp.MustCompile("something is wrong")) 76 | } 77 | -------------------------------------------------------------------------------- /parsecmd/symbols.go: -------------------------------------------------------------------------------- 1 | package parsecmd 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 symbolsCmd struct { 12 | path string 13 | apk string 14 | manifest string 15 | aapt string 16 | skipOsCheck bool 17 | } 18 | 19 | func (s *symbolsCmd) run(e *parsecli.Env, c *parsecli.Context) error { 20 | android := &androidSymbolUploader{ 21 | Path: s.path, 22 | Apk: s.apk, 23 | Manifest: s.manifest, 24 | AAPT: s.aapt} 25 | ios := &iosSymbolUploader{ 26 | Path: s.path, 27 | SkipOsCheck: s.skipOsCheck} 28 | switch { 29 | case android.acceptsPath(): 30 | if err := android.validate(); err != nil { 31 | return err 32 | } 33 | fmt.Fprintln(e.Out, "Uploading Android symbol files...") 34 | return android.uploadSymbols(e) 35 | case ios.acceptsPath(): 36 | if err := ios.validate(); err != nil { 37 | return err 38 | } 39 | fmt.Fprintln(e.Out, "Uploading iOS symbol files...") 40 | return ios.uploadSymbols(e) 41 | default: 42 | if s.path == "" { 43 | return stackerr.New("Please specify path to symbol files") 44 | } 45 | return stackerr.Newf("Do not understand symbol files at : %s", s.path) 46 | } 47 | } 48 | 49 | func NewSymbolsCmd(e *parsecli.Env) *cobra.Command { 50 | var s symbolsCmd 51 | cmd := &cobra.Command{ 52 | Use: "symbols [app]", 53 | Short: "Uploads symbol files", 54 | Long: `Uploads the symbol files for the application to symbolicate crash reports with. 55 | Path specifies the path to xcarchive/dSYM/DWARF for iOS or mapping.txt for Android.`, 56 | Run: parsecli.RunWithClient(e, s.run), 57 | } 58 | cmd.Flags().StringVarP(&s.path, "path", "p", s.path, 59 | "Path to symbols files") 60 | cmd.Flags().StringVarP(&s.apk, "apk", "a", s.apk, 61 | "Path to apk file") 62 | cmd.Flags().StringVarP(&s.manifest, "manifest", "m", s.manifest, 63 | "Path to AndroidManifest.xml file") 64 | cmd.Flags().StringVarP(&s.aapt, "aapt", "t", s.aapt, 65 | "Path to aapt for android") 66 | return cmd 67 | } 68 | -------------------------------------------------------------------------------- /parsecmd/symbols_test.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/ParsePlatform/parse-cli/parsecli" 8 | "github.com/facebookgo/ensure" 9 | ) 10 | 11 | func TestRunSymbolsCmd(t *testing.T) { 12 | t.Parallel() 13 | 14 | h := parsecli.NewHarness(t) 15 | defer h.Stop() 16 | 17 | s := &symbolsCmd{} 18 | ensure.Err(t, s.run(h.Env, nil), regexp.MustCompile("Please specify path")) 19 | } 20 | -------------------------------------------------------------------------------- /parsecmd/templates.go: -------------------------------------------------------------------------------- 1 | package parsecmd 2 | 3 | const ( 4 | sampleAppJS = ` 5 | // These two lines are required to initialize Express in Cloud Code. 6 | express = require('express'); 7 | app = express(); 8 | 9 | // Global app configuration section 10 | app.set('views', 'cloud/views'); // Specify the folder to find templates 11 | app.set('view engine', 'ejs'); // Set the template engine 12 | app.use(express.bodyParser()); // Middleware for reading request body 13 | 14 | // This is an example of hooking up a request handler with a specific request 15 | // path and HTTP verb using the Express routing API. 16 | app.get('/hello', function(req, res) { 17 | res.render('hello', { message: 'Congrats, you just set up your app!' }); 18 | }); 19 | 20 | // // Example reading from the request query string of an HTTP get request. 21 | // app.get('/test', function(req, res) { 22 | // // GET http://example.parseapp.com/test?message=hello 23 | // res.send(req.query.message); 24 | // }); 25 | 26 | // // Example reading from the request body of an HTTP post request. 27 | // app.post('/test', function(req, res) { 28 | // // POST http://example.parseapp.com/test (with request body "message=hello") 29 | // res.send(req.body.message); 30 | // }); 31 | 32 | // Attach the Express app to Cloud Code. 33 | app.listen(); 34 | ` 35 | 36 | helloEJS = ` 37 | 38 | 39 | 40 | Sample App 41 | 42 | 43 |

Hello World

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 | --------------------------------------------------------------------------------