Repository: exercism/cli
Pull request #564 (2018-06-22 02:58:21)

Infer site URL from configured API URL
We need to be able to show the user the correct site URL in error messages, depending on the environment that the CLI is configured for (e.g. development, staging, production, mentors beta).

This is kind of gross, but I expect to do a rewrite of the config logic, which will give us a chance to clean it all up.

@nywilken I'm going to merge it as soon as it goes green, to unblock some of the beta testing.


Patch:
diff --git a/api/client.go b/api/client.go
index 39671ac..e71a291 100644
--- a/api/client.go
+++ b/api/client.go
@@ -87,7 +87,8 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
 		// TODO: if it's json, and it has an error key, print the message.
 		return nil, fmt.Errorf("%s", res.Status)
 	case http.StatusUnauthorized:
-		return nil, fmt.Errorf("unauthorized request. Please run the configure command to set the api token found at https://v2.exercism.io/my/settings")
+		siteURL := config.InferSiteURL(c.APIConfig.BaseURL)
+		return nil, fmt.Errorf("unauthorized request. Please run the configure command. You can find your API token at %s/my/settings", siteURL)
 	default:
 		if v != nil {
 			defer res.Body.Close()
diff --git a/cli/status.go b/cli/status.go
index b9eded6..170becf 100644
--- a/cli/status.go
+++ b/cli/status.go
@@ -69,7 +69,13 @@ func NewStatus(c *CLI, uc config.UserConfig) Status {
 func (status *Status) Check() (string, error) {
 	status.Version = newVersionStatus(status.cli)
 	status.System = newSystemStatus()
-	status.Configuration = newConfigurationStatus(status)
+
+	cs, err := newConfigurationStatus(status)
+	if err != nil {
+		return "", err
+	}
+	status.Configuration = cs
+
 	ar, err := newAPIReachabilityStatus()
 	if err != nil {
 		return "", err
@@ -94,6 +100,7 @@ func newAPIReachabilityStatus() (apiReachabilityStatus, error) {
 	if err != nil {
 		return apiReachabilityStatus{}, nil
 	}
+	apiCfg.SetDefaults()
 	ar := apiReachabilityStatus{
 		Services: []*apiPing{
 			{Service: "GitHub", URL: "https://api.github.com"},
@@ -137,18 +144,23 @@ func newSystemStatus() systemStatus {
 	return ss
 }
 
-func newConfigurationStatus(status *Status) configurationStatus {
+func newConfigurationStatus(status *Status) (configurationStatus, error) {
+	apiCfg, err := config.NewAPIConfig()
+	if err != nil {
+		return configurationStatus{}, err
+	}
+	apiCfg.SetDefaults()
 	cs := configurationStatus{
 		Home:      status.cfg.Home,
 		Workspace: status.cfg.Workspace,
 		File:      status.cfg.File(),
 		Token:     status.cfg.Token,
-		TokenURL:  "http://exercism.io/account/key",
+		TokenURL:  config.InferSiteURL(apiCfg.BaseURL) + "/my/settings",
 	}
 	if status.Censor && status.cfg.Token != "" {
 		cs.Token = redactToken(status.cfg.Token)
 	}
-	return cs
+	return cs, nil
 }
 
 func (ping *apiPing) Call(wg *sync.WaitGroup) {
diff --git a/cmd/configure.go b/cmd/configure.go
index fa9abd4..33d8630 100644
--- a/cmd/configure.go
+++ b/cmd/configure.go
@@ -39,6 +39,7 @@ You can also override certain default settings to suit your preferences.
 		if err != nil {
 			return err
 		}
+		apiCfg.SetDefaults()
 
 		show, err := cmd.Flags().GetBool("show")
 		if err != nil {
@@ -67,7 +68,7 @@ You can also override certain default settings to suit your preferences.
 }
 
 func initConfigureCmd() {
-	configureCmd.Flags().StringP("token", "t", "", "authentication token used to connect to exercism.io")
+	configureCmd.Flags().StringP("token", "t", "", "authentication token used to connect to the site")
 	configureCmd.Flags().StringP("workspace", "w", "", "directory for exercism exercises")
 	configureCmd.Flags().StringP("api", "a", "", "API base url")
 	configureCmd.Flags().BoolP("show", "s", false, "show the current configuration")
diff --git a/cmd/download.go b/cmd/download.go
index 2a5da68..1ada384 100644
--- a/cmd/download.go
+++ b/cmd/download.go
@@ -3,11 +3,11 @@ package cmd
 import (
 	"errors"
 	"fmt"
-	"strings"
 	"io"
 	"net/http"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"github.com/exercism/cli/api"
 	"github.com/exercism/cli/config"
@@ -210,7 +210,7 @@ type downloadPayload struct {
 func initDownloadCmd() {
 	downloadCmd.Flags().StringP("uuid", "u", "", "the solution UUID")
 	downloadCmd.Flags().StringP("track", "t", "", "the track ID")
-	downloadCmd.Flags().StringP("token", "k", "", "authentication token used to connect to exercism.io")
+	downloadCmd.Flags().StringP("token", "k", "", "authentication token used to connect to the site")
 }
 
 func init() {
diff --git a/cmd/root.go b/cmd/root.go
index 214d53e..17c1301 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -29,7 +29,7 @@ var (
 var RootCmd = &cobra.Command{
 	Use:   BinaryName,
 	Short: "A friendly command-line interface to Exercism.",
-	Long: `A command-line interface for https://v2.exercism.io.
+	Long: `A command-line interface for the v2 redesign of Exercism.
 
 Download exercises and submit your solutions.`,
 	SilenceUsage: true,
diff --git a/config/config.go b/config/config.go
index 9318b9b..d71f0da 100644
--- a/config/config.go
+++ b/config/config.go
@@ -6,6 +6,7 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"regexp"
 
 	"github.com/spf13/viper"
 )
@@ -60,3 +61,13 @@ func ensureDir(f filer) error {
 	}
 	return err
 }
+
+// InferSiteURL guesses what the website URL is.
+// The basis for the guess is which API we're submitting to.
+func InferSiteURL(apiURL string) string {
+	if apiURL == "https://api.exercism.io/v1" {
+		return "https://exercism.io"
+	}
+	re := regexp.MustCompile("^(https?://[^/]*).*")
+	return re.ReplaceAllString(apiURL, "$1")
+}


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index 1fe086d..44808d5 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -95,3 +95,19 @@ func TestFakeConfig(t *testing.T) {
 	assert.Equal(t, "b", cfg.Letter)
 	assert.Equal(t, 1, cfg.Number)
 }
+
+func TestInferSiteURL(t *testing.T) {
+	testCases := []struct {
+		api, url string
+	}{
+		{"https://api.exercism.io/v1", "https://exercism.io"},
+		{"https://v2.exercism.io/api/v1", "https://v2.exercism.io"},
+		{"https://mentors-beta.exercism.io/api/v1", "https://mentors-beta.exercism.io"},
+		{"http://localhost:3000/api/v1", "http://localhost:3000"},
+		{"http://whatever", "http://whatever"}, // you're on your own, pal
+	}
+
+	for _, tc := range testCases {
+		assert.Equal(t, InferSiteURL(tc.api), tc.url)
+	}
+}