diff --git a/README.md b/README.md index 4a0044b..6459e8c 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Run `bootdev --version` on your command line to make sure the installation worke **Optional troubleshooting:** -If you're getting a "command not found" error for `bootdev help`, it's most likely because the directory containing the `bootdev` program isn't in your [`PATH`](https://opensource.com/article/17/6/set-path-linux). You need to add the directory to your `PATH` by modifying your shell's configuration file. You probably need to add `$HOME/go/bin` (the default `GOBIN` directory where `go` installs programs) to your `PATH`: +If you're getting a "command not found" error for `bootdev`, it's likely because the directory containing the `bootdev` program isn't in your [`PATH`](https://opensource.com/article/17/6/set-path-linux). You need to add the directory to `PATH` by modifying your shell's configuration file. In most cases, this means adding `$HOME/go/bin` (the default `GOBIN` directory where `go` installs programs): ```sh # For Linux/WSL @@ -127,7 +127,7 @@ For lessons with HTTP tests, you can configure the CLI with a base URL that over - To set the base URL, run: ```sh - bootdev config base_url + bootdev config base_url YOUR_URL ``` _Make sure you include the protocol scheme (`http://`) in the URL._ @@ -151,10 +151,10 @@ The CLI text output is rendered with extra colors: green (e.g., success messages - To customize these colors, run: ```sh - bootdev config colors --red --green --gray + bootdev config colors --red VALUE --green VALUE --gray VALUE ``` - _You can use an [ANSI color code](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) or a hex string as the ``._ + _You can use an [ANSI color code](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) or a hex string as the `VALUE`._ - To get the current colors, run: diff --git a/checks/http.go b/checks/http.go index fa95580..a9f2369 100644 --- a/checks/http.go +++ b/checks/http.go @@ -34,7 +34,8 @@ func runHTTPRequest( dat, err := json.Marshal(requestStep.Request.BodyJSON) cobra.CheckErr(err) interpolatedBodyJSONStr := InterpolateVariables(string(dat), variables) - req, err = http.NewRequest(requestStep.Request.Method, completeURL, + req, err = http.NewRequest( + requestStep.Request.Method, completeURL, bytes.NewBuffer([]byte(interpolatedBodyJSONStr)), ) if err != nil { @@ -50,7 +51,8 @@ func runHTTPRequest( encodedFormStr := formValues.Encode() var err error - req, err = http.NewRequest(requestStep.Request.Method, completeURL, + req, err = http.NewRequest( + requestStep.Request.Method, completeURL, strings.NewReader(encodedFormStr), ) if err != nil { diff --git a/checks/http_test.go b/checks/http_test.go new file mode 100644 index 0000000..0082e52 --- /dev/null +++ b/checks/http_test.go @@ -0,0 +1,128 @@ +package checks + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + api "github.com/bootdotdev/bootdev/client" +) + +func TestInterpolateVariables(t *testing.T) { + got := InterpolateVariables( + "${baseURL}/users/${id}?missing=${missing}", + map[string]string{"baseURL": "http://localhost:8080", "id": "42"}, + ) + want := "http://localhost:8080/users/42?missing=${missing}" + if got != want { + t.Fatalf("InterpolateVariables() = %q, want %q", got, want) + } +} + +func TestInterpolationNames(t *testing.T) { + got := InterpolationNames("${baseURL}/users/${id}/${id}") + want := []string{"baseURL", "id", "id"} + if len(got) != len(want) { + t.Fatalf("InterpolationNames() = %#v, want %#v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("InterpolationNames() = %#v, want %#v", got, want) + } + } +} + +func TestRunHTTPRequestInterpolatesRequestAndCapturesResponseVariables(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("method = %q, want %q", r.Method, http.MethodPost) + return + } + if r.URL.Path != "/users/42" { + t.Errorf("path = %q, want %q", r.URL.Path, "/users/42") + return + } + if r.Header.Get("X-User-ID") != "42" { + t.Errorf("X-User-ID = %q, want %q", r.Header.Get("X-User-ID"), "42") + return + } + + username, password, ok := r.BasicAuth() + if !ok || username != "user" || password != "pass" { + t.Errorf("BasicAuth() = %q, %q, %v; want user, pass, true", username, password, ok) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("failed reading request body: %v", err) + return + } + var payload map[string]string + if err := json.Unmarshal(body, &payload); err != nil { + t.Errorf("failed unmarshalling request body %q: %v", string(body), err) + return + } + if payload["message"] != "hello Theo" { + t.Errorf("message = %q, want %q", payload["message"], "hello Theo") + return + } + + w.Header().Set("X-Request-OK", "yes") + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte(`{"token":"abc123"}`)) + })) + defer server.Close() + + variables := map[string]string{ + "id": "42", + "name": "Theo", + } + requestStep := api.CLIStepHTTPRequest{ + ResponseVariables: []api.HTTPRequestResponseVariable{{Name: "token", Path: ".token"}}, + Request: api.HTTPRequest{ + Method: http.MethodPost, + FullURL: api.BaseURLPlaceholder + "/users/${id}", + Headers: map[string]string{ + "X-User-ID": "${id}", + }, + BodyJSON: map[string]any{ + "message": "hello ${name}", + }, + BasicAuth: &api.HTTPBasicAuth{Username: "user", Password: "pass"}, + }, + } + + result := runHTTPRequest(server.Client(), server.URL, variables, requestStep) + if result.Err != "" { + t.Fatalf("unexpected request error: %s", result.Err) + } + if result.StatusCode != http.StatusCreated { + t.Fatalf("StatusCode = %d, want %d", result.StatusCode, http.StatusCreated) + } + if result.ResponseHeaders["X-Request-Ok"] != "yes" { + t.Fatalf("ResponseHeaders[X-Request-Ok] = %q, want %q", result.ResponseHeaders["X-Request-Ok"], "yes") + } + if result.BodyString != `{"token":"abc123"}` { + t.Fatalf("BodyString = %q, want token response", result.BodyString) + } + if result.Variables["token"] != "abc123" { + t.Fatalf("captured token = %q, want %q", result.Variables["token"], "abc123") + } + if result.Variables["id"] != "42" { + t.Fatalf("original variable id = %q, want %q", result.Variables["id"], "42") + } +} + +func TestTruncateAndStringifyBodyCapsBinaryBody(t *testing.T) { + body := []byte(strings.Repeat("a", 20*1024)) + body[0] = 0 + + got := truncateAndStringifyBody(body) + if len(got) != 16*1024 { + t.Fatalf("len(truncateAndStringifyBody(binary)) = %d, want %d", len(got), 16*1024) + } +} diff --git a/checks/jq.go b/checks/jq.go index 568bd1c..aad5f34 100644 --- a/checks/jq.go +++ b/checks/jq.go @@ -14,7 +14,7 @@ import ( func prettyPrintStdoutJqTest(test api.StdoutJqTest, variables map[string]string) string { queryText := InterpolateVariables(test.Query, variables) var str strings.Builder - str.WriteString(fmt.Sprintf("Expect jq query '%s' to yield values satisfying:", queryText)) + fmt.Fprintf(&str, "Expect jq query '%s' to yield values satisfying:", queryText) if len(test.ExpectedResults) == 0 { str.WriteString("\n - [no expected results provided]") return str.String() diff --git a/checks/jq_test.go b/checks/jq_test.go new file mode 100644 index 0000000..8a8963f --- /dev/null +++ b/checks/jq_test.go @@ -0,0 +1,176 @@ +package checks + +import ( + "reflect" + "strings" + "testing" + + api "github.com/bootdotdev/bootdev/client" +) + +func TestRunStdoutJqQuery(t *testing.T) { + tests := []struct { + name string + stdout string + test api.StdoutJqTest + variables map[string]string + want api.CLICommandJqOutput + wantError string + }{ + { + name: "queries json with interpolated query", + stdout: `{"users":[{"name":"Lane"},{"name":"Theo"}]}`, + test: api.StdoutJqTest{ + InputMode: "json", + Query: `.users[] | select(.name == "${name}") | .name`, + }, + variables: map[string]string{"name": "Theo"}, + want: api.CLICommandJqOutput{ + Query: `.users[] | select(.name == "Theo") | .name`, + Results: []string{`"Theo"`}, + }, + }, + { + name: "queries jsonl as array", + stdout: "{\"id\":1}\n{\"id\":2}\n", + test: api.StdoutJqTest{ + InputMode: "jsonl", + Query: `.[].id`, + }, + want: api.CLICommandJqOutput{ + Query: `.[].id`, + Results: []string{`1`, `2`}, + }, + }, + { + name: "returns parse error", + stdout: `{not json}`, + test: api.StdoutJqTest{ + InputMode: "json", + Query: `.name`, + }, + want: api.CLICommandJqOutput{ + Query: `.name`, + }, + wantError: "invalid character", + }, + { + name: "returns jq error", + stdout: `{"name":"Theo"}`, + test: api.StdoutJqTest{ + InputMode: "json", + Query: `.name[`, + }, + want: api.CLICommandJqOutput{ + Query: `.name[`, + Error: "unexpected EOF", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := runStdoutJqQuery(tt.stdout, tt.test, tt.variables) + if tt.wantError != "" { + if got.Query != tt.want.Query { + t.Fatalf("Query = %q, want %q", got.Query, tt.want.Query) + } + if !strings.Contains(got.Error, tt.wantError) { + t.Fatalf("expected error containing %q, got %q", tt.wantError, got.Error) + } + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("runStdoutJqQuery() = %#v, want %#v", got, tt.want) + } + }) + } +} + +func TestParseJqInputRejectsMultipleJSONValuesInJSONMode(t *testing.T) { + _, err := parseJqInput("{\"id\":1}\n{\"id\":2}\n", "json") + if err == nil { + t.Fatal("expected error for multiple JSON values in json mode") + } + if err.Error() != "expected a single JSON value" { + t.Fatalf("expected single-value error, got %q", err.Error()) + } +} + +func TestFormatJqResults(t *testing.T) { + got := formatJqResults([]any{"hello", float64(42), true, nil, map[string]any{"id": float64(1)}}) + want := []string{`"hello"`, `42`, `true`, `null`, `{"id":1}`} + if !reflect.DeepEqual(got, want) { + t.Fatalf("formatJqResults() = %#v, want %#v", got, want) + } +} + +func TestFormatJqExpectedValueInterpolatesOnlyStrings(t *testing.T) { + variables := map[string]string{"name": "Theo"} + + gotString := formatJqExpectedValue(api.JqExpectedResult{ + Type: api.JqTypeString, + Value: "hello ${name}", + }, variables) + if gotString != `"hello Theo"` { + t.Fatalf("expected interpolated string value, got %q", gotString) + } + + gotInt := formatJqExpectedValue(api.JqExpectedResult{ + Type: api.JqTypeInt, + Value: "${name}", + }, variables) + if gotInt != `"${name}"` { + t.Fatalf("expected non-string jq type to avoid interpolation, got %q", gotInt) + } +} + +func TestValFromJqPath(t *testing.T) { + tests := []struct { + name string + path string + jsn string + want any + wantErr string + }{ + { + name: "returns one value", + path: `.token`, + jsn: `{"token":"abc123"}`, + want: "abc123", + }, + { + name: "errors on missing value", + path: `.missing`, + jsn: `{"token":"abc123"}`, + wantErr: "value not found", + }, + { + name: "errors on multiple values", + path: `.items[].id`, + jsn: `{"items":[{"id":1},{"id":2}]}`, + wantErr: "invalid number of values found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := valFromJqPath(tt.path, tt.jsn) + if tt.wantErr != "" { + if err == nil { + t.Fatalf("expected error %q", tt.wantErr) + } + if err.Error() != tt.wantErr { + t.Fatalf("expected error %q, got %q", tt.wantErr, err.Error()) + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("valFromJqPath() = %#v, want %#v", got, tt.want) + } + }) + } +} diff --git a/checks/runner_test.go b/checks/runner_test.go new file mode 100644 index 0000000..9161142 --- /dev/null +++ b/checks/runner_test.go @@ -0,0 +1,130 @@ +package checks + +import ( + "testing" + + api "github.com/bootdotdev/bootdev/client" + "github.com/bootdotdev/bootdev/messages" + tea "github.com/charmbracelet/bubbletea" +) + +func TestApplySubmissionResultsMarksAllStepsAndTestsPassedWhenNoFailure(t *testing.T) { + cliData := api.CLIData{Steps: []api.CLIStep{ + {CLICommand: &api.CLIStepCLICommand{Tests: []api.CLICommandTest{{}, {}}}}, + {HTTPRequest: &api.CLIStepHTTPRequest{Tests: []api.HTTPRequestTest{{}}}}, + }} + + got := applySubmissionResultsMessages(cliData, nil) + want := []tea.Msg{ + messages.ResolveStepMsg{Index: 0, Passed: boolPtr(true)}, + messages.ResolveTestMsg{StepIndex: 0, TestIndex: 0, Passed: boolPtr(true)}, + messages.ResolveTestMsg{StepIndex: 0, TestIndex: 1, Passed: boolPtr(true)}, + messages.ResolveStepMsg{Index: 1, Passed: boolPtr(true)}, + messages.ResolveTestMsg{StepIndex: 1, TestIndex: 0, Passed: boolPtr(true)}, + } + + assertMessages(t, got, want) +} + +func TestApplySubmissionResultsStopsAfterFailedCLITest(t *testing.T) { + cliData := api.CLIData{Steps: []api.CLIStep{ + {CLICommand: &api.CLIStepCLICommand{Tests: []api.CLICommandTest{{}}}}, + {CLICommand: &api.CLIStepCLICommand{Tests: []api.CLICommandTest{{}, {}, {}}}}, + {CLICommand: &api.CLIStepCLICommand{Tests: []api.CLICommandTest{{}}}}, + }} + failure := &api.StructuredErrCLI{FailedStepIndex: 1, FailedTestIndex: 1} + + got := applySubmissionResultsMessages(cliData, failure) + want := []tea.Msg{ + messages.ResolveStepMsg{Index: 0, Passed: boolPtr(true)}, + messages.ResolveTestMsg{StepIndex: 0, TestIndex: 0, Passed: boolPtr(true)}, + messages.ResolveStepMsg{Index: 1, Passed: boolPtr(false)}, + messages.ResolveTestMsg{StepIndex: 1, TestIndex: 0, Passed: boolPtr(true)}, + messages.ResolveTestMsg{StepIndex: 1, TestIndex: 1, Passed: boolPtr(false)}, + } + + assertMessages(t, got, want) +} + +func TestApplySubmissionResultsStopsAfterFailedHTTPTest(t *testing.T) { + cliData := api.CLIData{Steps: []api.CLIStep{ + {CLICommand: &api.CLIStepCLICommand{Tests: []api.CLICommandTest{{}}}}, + {HTTPRequest: &api.CLIStepHTTPRequest{Tests: []api.HTTPRequestTest{{}, {}, {}}}}, + }} + failure := &api.StructuredErrCLI{FailedStepIndex: 1, FailedTestIndex: 1} + + got := applySubmissionResultsMessages(cliData, failure) + want := []tea.Msg{ + messages.ResolveStepMsg{Index: 0, Passed: boolPtr(true)}, + messages.ResolveTestMsg{StepIndex: 0, TestIndex: 0, Passed: boolPtr(true)}, + messages.ResolveStepMsg{Index: 1, Passed: boolPtr(false)}, + messages.ResolveTestMsg{StepIndex: 1, TestIndex: 0, Passed: boolPtr(true)}, + messages.ResolveTestMsg{StepIndex: 1, TestIndex: 1, Passed: boolPtr(false)}, + } + + assertMessages(t, got, want) +} + +func applySubmissionResultsMessages(cliData api.CLIData, failure *api.StructuredErrCLI) []tea.Msg { + ch := make(chan tea.Msg) + done := make(chan struct{}) + go func() { + defer close(ch) + defer close(done) + ApplySubmissionResults(cliData, failure, ch) + }() + + var msgs []tea.Msg + for msg := range ch { + msgs = append(msgs, msg) + } + <-done + return msgs +} + +func assertMessages(t *testing.T, got []tea.Msg, want []tea.Msg) { + t.Helper() + + if len(got) != len(want) { + t.Fatalf("got %d messages, want %d\ngot: %#v\nwant: %#v", len(got), len(want), got, want) + } + for i := range want { + assertMessage(t, i, got[i], want[i]) + } +} + +func assertMessage(t *testing.T, index int, got tea.Msg, want tea.Msg) { + t.Helper() + + switch want := want.(type) { + case messages.ResolveStepMsg: + got, ok := got.(messages.ResolveStepMsg) + if !ok { + t.Fatalf("message %d = %T, want %T", index, got, want) + } + if got.Index != want.Index || !sameBoolPtr(got.Passed, want.Passed) { + t.Fatalf("message %d = %#v, want %#v", index, got, want) + } + case messages.ResolveTestMsg: + got, ok := got.(messages.ResolveTestMsg) + if !ok { + t.Fatalf("message %d = %T, want %T", index, got, want) + } + if got.StepIndex != want.StepIndex || got.TestIndex != want.TestIndex || !sameBoolPtr(got.Passed, want.Passed) { + t.Fatalf("message %d = %#v, want %#v", index, got, want) + } + default: + t.Fatalf("unsupported wanted message type %T", want) + } +} + +func boolPtr(v bool) *bool { + return &v +} + +func sameBoolPtr(a *bool, b *bool) bool { + if a == nil || b == nil { + return a == b + } + return *a == *b +} diff --git a/checks/tmdl.go b/checks/tmdl.go index 5ef9337..7620cb1 100644 --- a/checks/tmdl.go +++ b/checks/tmdl.go @@ -4,9 +4,9 @@ import "strings" const tabWidth = 4 -// Find the first line whose left-trimmed text has prefix `query`, and return the -// "item block": that line plus following lines until the next non-empty line with -// indent <= the matched line's indent +// ExtractTmdlBlock finds the first line whose left-trimmed text has prefix `query` and +// returns the "item block": that line plus following lines until the next non-empty +// line with indent <= the matched line's indent. func ExtractTmdlBlock(input, query string) string { trimmedQuery := strings.TrimSpace(query) if trimmedQuery == "" { diff --git a/checks/tmdl_test.go b/checks/tmdl_test.go new file mode 100644 index 0000000..08a9499 --- /dev/null +++ b/checks/tmdl_test.go @@ -0,0 +1,53 @@ +package checks + +import "testing" + +func TestExtractTmdlBlock(t *testing.T) { + input := "root\n child one\n grandchild\n\n child two\nnext root" + + tests := []struct { + name string + query string + want string + }{ + { + name: "blank query returns input", + query: " ", + want: input, + }, + { + name: "missing query returns empty string", + query: "missing", + want: "", + }, + { + name: "extracts matching item block", + query: "child one", + want: " child one\n grandchild", + }, + { + name: "blank lines do not terminate block", + query: "root", + want: "root\n child one\n grandchild\n\n child two", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ExtractTmdlBlock(input, tt.query) + if got != tt.want { + t.Fatalf("ExtractTmdlBlock() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestExtractTmdlBlockHandlesTabIndentation(t *testing.T) { + input := "item\n\tchild\n\t\tgrandchild\n\tsibling\nnext" + + got := ExtractTmdlBlock(input, "child") + want := "\tchild\n\t\tgrandchild" + if got != want { + t.Fatalf("ExtractTmdlBlock() = %q, want %q", got, want) + } +} diff --git a/client/auth.go b/client/auth.go index 2131782..000a946 100644 --- a/client/auth.go +++ b/client/auth.go @@ -25,13 +25,13 @@ type CurrentUserResponse struct { } func FetchAccessToken() (*LoginResponse, error) { - api_url := viper.GetString("api_url") + apiURL := viper.GetString("api_url") client := &http.Client{} - r, err := http.NewRequest("POST", api_url+"/v1/auth/refresh", bytes.NewBuffer([]byte{})) - r.Header.Add("X-Refresh-Token", viper.GetString("refresh_token")) + r, err := http.NewRequest("POST", apiURL+"/v1/auth/refresh", bytes.NewBuffer([]byte{})) if err != nil { return nil, err } + r.Header.Add("X-Refresh-Token", viper.GetString("refresh_token")) resp, err := client.Do(r) if err != nil { return nil, err @@ -53,16 +53,17 @@ func FetchAccessToken() (*LoginResponse, error) { } func LoginWithCode(code string) (*LoginResponse, error) { - api_url := viper.GetString("api_url") + apiURL := viper.GetString("api_url") req, err := json.Marshal(LoginRequest{Otp: code}) if err != nil { return nil, err } - resp, err := http.Post(api_url+"/v1/auth/otp/login", "application/json", bytes.NewReader(req)) + resp, err := http.Post(apiURL+"/v1/auth/otp/login", "application/json", bytes.NewReader(req)) if err != nil { return nil, err } + defer resp.Body.Close() if resp.StatusCode == 403 { return nil, errors.New("invalid login code; please refresh your browser and try again") @@ -116,9 +117,9 @@ func fetchWithAuth(method string, url string) ([]byte, error) { } func fetchWithAuthAndPayload(method string, url string, payload []byte) ([]byte, int, error) { - api_url := viper.GetString("api_url") + apiURL := viper.GetString("api_url") client := &http.Client{} - r, err := http.NewRequest(method, api_url+url, bytes.NewBuffer(payload)) + r, err := http.NewRequest(method, apiURL+url, bytes.NewBuffer(payload)) if err != nil { return nil, 0, err } diff --git a/client/lessons.go b/client/lessons.go index 6ae6b45..f4ce9bf 100644 --- a/client/lessons.go +++ b/client/lessons.go @@ -112,7 +112,7 @@ type HTTPRequestResponseVariable struct { Path string } -// Only one of these fields should be set +// HTTPRequestTest should have only one field set type HTTPRequestTest struct { StatusCode *int BodyContains *string diff --git a/cmd/login.go b/cmd/login.go index dc453b6..a83eed6 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -47,9 +47,9 @@ var loginCmd = &cobra.Command{ fmt.Print("Welcome to the Boot.dev CLI!\n\n") } - loginUrl := viper.GetString("frontend_url") + "/cli/login" + loginURL := viper.GetString("frontend_url") + "/cli/login" - fmt.Println("Please navigate to:\n" + loginUrl) + fmt.Println("Please navigate to:\n" + loginURL) inputChan := make(chan string) @@ -68,7 +68,7 @@ var loginCmd = &cobra.Command{ go func() { browser.Stdout = nil browser.Stderr = nil - browser.OpenURL(loginUrl) + browser.OpenURL(loginURL) }() // race the web server against the user's input @@ -123,8 +123,8 @@ func startHTTPServer(inputChan chan string) { } handleRedirect := func(w http.ResponseWriter, r *http.Request) { - loginUrl := viper.GetString("frontend_url") + "/cli/login" - http.Redirect(w, r, loginUrl, http.StatusSeeOther) + loginURL := viper.GetString("frontend_url") + "/cli/login" + http.Redirect(w, r, loginURL, http.StatusSeeOther) } http.Handle("POST /submit", cors(http.HandlerFunc(handleSubmit))) diff --git a/cmd/logout.go b/cmd/logout.go index fd22d62..2311470 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -11,10 +11,10 @@ import ( ) func logout() { - api_url := viper.GetString("api_url") + apiURL := viper.GetString("api_url") client := &http.Client{} // Best effort - logout should never fail - r, _ := http.NewRequest("POST", api_url+"/v1/auth/logout", bytes.NewBuffer([]byte{})) + r, _ := http.NewRequest("POST", apiURL+"/v1/auth/logout", bytes.NewBuffer([]byte{})) r.Header.Add("X-Refresh-Token", viper.GetString("refresh_token")) client.Do(r) diff --git a/cmd/root.go b/cmd/root.go index 758b7a2..91a541d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -172,12 +172,12 @@ func requireAuth(cmd *cobra.Command, args []string) { } } - access_token := viper.GetString("access_token") - promptLoginAndExitIf(access_token == "") + accessToken := viper.GetString("access_token") + promptLoginAndExitIf(accessToken == "") // We only refresh if our token is getting stale. - last_refresh := viper.GetInt64("last_refresh") - if time.Now().Add(-time.Minute*55).Unix() <= last_refresh { + lastRefresh := viper.GetInt64("last_refresh") + if time.Now().Add(-time.Minute*55).Unix() <= lastRefresh { return }