From 8d270e8830463356ce1e72be67cdb7cadc9edaf9 Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Tue, 17 Mar 2026 07:02:14 -0700 Subject: [PATCH 1/2] typed errors --- internal/api/api_key.go | 4 +-- internal/api/api_key_test.go | 52 +++++++++++++++++++----------------- internal/api/client.go | 9 +++++++ 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/internal/api/api_key.go b/internal/api/api_key.go index 6d25964..d2e705f 100644 --- a/internal/api/api_key.go +++ b/internal/api/api_key.go @@ -23,11 +23,11 @@ func (c *Client) GetAPIKey() (*APIKeyResponse, error) { defer resp.Body.Close() if resp.StatusCode == http.StatusUnauthorized { - return nil, fmt.Errorf("invalid API key") + return nil, &APIError{StatusCode: resp.StatusCode, Message: "invalid API key"} } if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) + return nil, &APIError{StatusCode: resp.StatusCode, Message: fmt.Sprintf("unexpected status: %d", resp.StatusCode)} } var result APIKeyResponse diff --git a/internal/api/api_key_test.go b/internal/api/api_key_test.go index 5c9b403..9aaaad2 100644 --- a/internal/api/api_key_test.go +++ b/internal/api/api_key_test.go @@ -1,42 +1,45 @@ package api import ( + "errors" "net/http" "net/http/httptest" + "strings" "testing" ) func TestGetAPIKey(t *testing.T) { tests := []struct { - name string - statusCode int - body string - wantErr string - wantTeam string + name string + statusCode int + body string + wantAPIErr *APIError + wantErrMsg string + wantTeam string }{ { name: "success", statusCode: http.StatusOK, - body: `{"success":true,"teamName":"Acme"}`, + body: `{"teamName":"Acme"}`, wantTeam: "Acme", }, { name: "unauthorized", statusCode: http.StatusUnauthorized, body: `{"success":false,"error":"Invalid API key"}`, - wantErr: "invalid API key", + wantAPIErr: &APIError{StatusCode: http.StatusUnauthorized}, }, { name: "unexpected status", statusCode: http.StatusInternalServerError, body: ``, - wantErr: "unexpected status: 500", + wantAPIErr: &APIError{StatusCode: http.StatusInternalServerError}, }, { name: "invalid json", statusCode: http.StatusOK, body: `not json`, - wantErr: "failed to decode response", + wantErrMsg: "failed to decode response", }, } @@ -51,12 +54,23 @@ func TestGetAPIKey(t *testing.T) { client := NewClient(server.URL, "test-key") result, err := client.GetAPIKey() - if tt.wantErr != "" { + if tt.wantAPIErr != nil { + var apiErr *APIError + if !errors.As(err, &apiErr) { + t.Fatalf("expected *APIError, got %T: %v", err, err) + } + if apiErr.StatusCode != tt.wantAPIErr.StatusCode { + t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, tt.wantAPIErr.StatusCode) + } + return + } + + if tt.wantErrMsg != "" { if err == nil { - t.Fatalf("expected error containing %q, got nil", tt.wantErr) + t.Fatalf("expected error containing %q, got nil", tt.wantErrMsg) } - if !contains(err.Error(), tt.wantErr) { - t.Errorf("error = %q, want it to contain %q", err.Error(), tt.wantErr) + if !strings.Contains(err.Error(), tt.wantErrMsg) { + t.Errorf("error = %q, want it to contain %q", err.Error(), tt.wantErrMsg) } return } @@ -70,15 +84,3 @@ func TestGetAPIKey(t *testing.T) { }) } } - -func contains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || len(substr) == 0 || - func() bool { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - return false - }()) -} diff --git a/internal/api/client.go b/internal/api/client.go index d08fefe..608ba69 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -6,6 +6,15 @@ import ( "time" ) +type APIError struct { + StatusCode int + Message string +} + +func (e *APIError) Error() string { + return e.Message +} + type Client struct { baseURL string apiKey string From 3607dda68e055081325f6a59a52db7bcc47bd471 Mon Sep 17 00:00:00 2001 From: Nate Meyer <672246+notnmeyer@users.noreply.github.com> Date: Tue, 17 Mar 2026 08:27:11 -0700 Subject: [PATCH 2/2] make sure to use the error message from the api --- internal/api/api_key.go | 6 +----- internal/api/api_key_test.go | 5 ++++- internal/api/client.go | 11 +++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/internal/api/api_key.go b/internal/api/api_key.go index d2e705f..72116dd 100644 --- a/internal/api/api_key.go +++ b/internal/api/api_key.go @@ -22,12 +22,8 @@ func (c *Client) GetAPIKey() (*APIKeyResponse, error) { } defer resp.Body.Close() - if resp.StatusCode == http.StatusUnauthorized { - return nil, &APIError{StatusCode: resp.StatusCode, Message: "invalid API key"} - } - if resp.StatusCode != http.StatusOK { - return nil, &APIError{StatusCode: resp.StatusCode, Message: fmt.Sprintf("unexpected status: %d", resp.StatusCode)} + return nil, errorFromResponse(resp) } var result APIKeyResponse diff --git a/internal/api/api_key_test.go b/internal/api/api_key_test.go index 9aaaad2..63d8a3e 100644 --- a/internal/api/api_key_test.go +++ b/internal/api/api_key_test.go @@ -27,7 +27,7 @@ func TestGetAPIKey(t *testing.T) { name: "unauthorized", statusCode: http.StatusUnauthorized, body: `{"success":false,"error":"Invalid API key"}`, - wantAPIErr: &APIError{StatusCode: http.StatusUnauthorized}, + wantAPIErr: &APIError{StatusCode: http.StatusUnauthorized, Message: "Invalid API key"}, }, { name: "unexpected status", @@ -62,6 +62,9 @@ func TestGetAPIKey(t *testing.T) { if apiErr.StatusCode != tt.wantAPIErr.StatusCode { t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, tt.wantAPIErr.StatusCode) } + if tt.wantAPIErr.Message != "" && apiErr.Message != tt.wantAPIErr.Message { + t.Errorf("Message = %q, want %q", apiErr.Message, tt.wantAPIErr.Message) + } return } diff --git a/internal/api/client.go b/internal/api/client.go index 608ba69..74023ba 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -1,6 +1,7 @@ package api import ( + "encoding/json" "fmt" "net/http" "time" @@ -29,6 +30,16 @@ func NewClient(baseURL, apiKey string) *Client { } } +func errorFromResponse(resp *http.Response) *APIError { + var body struct { + Error string `json:"error"` + } + if err := json.NewDecoder(resp.Body).Decode(&body); err == nil && body.Error != "" { + return &APIError{StatusCode: resp.StatusCode, Message: body.Error} + } + return &APIError{StatusCode: resp.StatusCode, Message: fmt.Sprintf("unexpected status: %d", resp.StatusCode)} +} + func (c *Client) newRequest(method, path string) (*http.Request, error) { url := fmt.Sprintf("%s%s", c.baseURL, path) req, err := http.NewRequest(method, url, nil)