Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions Types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package keymint

import "fmt"
import (
"encoding/json"
"fmt"
)

// NewCustomer represents the structure for creating a new customer when creating a license key.
type NewCustomer struct {
Expand Down Expand Up @@ -88,8 +91,37 @@ type ApiError struct {
Code int `json:"code"`
// Status is the optional HTTP status code.
Status *int `json:"status,omitempty"`
// ErrorDetail holds the nested error envelope when present.
// ErrorDetail holds the nested error envelope when `error` is an object.
ErrorDetail *ApiErrorDetails `json:"error,omitempty"`
// ErrorText holds `error` when the API sends it as a plain string
// (e.g. "Method not allowed") instead of an object.
ErrorText string `json:"-"`
}

// UnmarshalJSON accepts the `error` field as either a string or an object,
// since both shapes appear in Keymint API error responses.
func (e *ApiError) UnmarshalJSON(data []byte) error {
type apiErrorFields ApiError
var raw struct {
*apiErrorFields
Error json.RawMessage `json:"error,omitempty"`
}
raw.apiErrorFields = (*apiErrorFields)(e)
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
if len(raw.Error) == 0 || string(raw.Error) == "null" {
return nil
}
if raw.Error[0] == '"' {
return json.Unmarshal(raw.Error, &e.ErrorText)
}
var detail ApiErrorDetails
if err := json.Unmarshal(raw.Error, &detail); err != nil {
return err
}
e.ErrorDetail = &detail
return nil
}

// Error implements the error interface for ApiError.
Expand Down
58 changes: 58 additions & 0 deletions contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,64 @@ func TestApiErrorsExposeNestedMessage(t *testing.T) {
}
}

func TestApiErrorUnmarshalsErrorFieldShapes(t *testing.T) {
cases := []struct {
name string
body string
wantText string
wantDetail string
}{
{"string", `{"message":"Method not allowed","code":1,"error":"Method not allowed"}`, "Method not allowed", ""},
{"object", `{"message":"Too many requests","code":1,"error":{"code":"RATE_LIMIT_EXCEEDED","message":"Too many requests"}}`, "", "RATE_LIMIT_EXCEEDED"},
{"null", `{"message":"Invalid license key","code":1,"error":null}`, "", ""},
{"absent", `{"message":"Invalid license key","code":1}`, "", ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var apiErr ApiError
if err := json.Unmarshal([]byte(tc.body), &apiErr); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
if apiErr.Message == "" || apiErr.Code != 1 {
t.Fatalf("top-level fields not decoded: %#v", apiErr)
}
if apiErr.ErrorText != tc.wantText {
t.Fatalf("expected ErrorText %q, got %q", tc.wantText, apiErr.ErrorText)
}
switch {
case tc.wantDetail == "" && apiErr.ErrorDetail != nil:
t.Fatalf("expected nil ErrorDetail, got %#v", apiErr.ErrorDetail)
case tc.wantDetail != "" && (apiErr.ErrorDetail == nil || apiErr.ErrorDetail.Code == nil || *apiErr.ErrorDetail.Code != tc.wantDetail):
t.Fatalf("expected ErrorDetail code %q, got %#v", tc.wantDetail, apiErr.ErrorDetail)
}
})
}
}

func TestApiErrorsAcceptStringErrorField(t *testing.T) {
c, done := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`{"error":"This API key is not allowed from this IP address","message":"This API key is not allowed from this IP address","code":1}`))
})
defer done()

_, err := c.ActivateKey(ActivateKeyParams{ProductID: "p", LicenseKey: "k"})
apiErr, ok := err.(*ApiError)
if !ok {
t.Fatalf("expected *ApiError, got %T", err)
}
if apiErr.Code != 1 || apiErr.Message != "This API key is not allowed from this IP address" {
t.Fatalf("string error body not decoded: %#v", apiErr)
}
if apiErr.ErrorText != apiErr.Message || apiErr.ErrorDetail != nil {
t.Fatalf("unexpected error field: %#v", apiErr)
}
if apiErr.Status == nil || *apiErr.Status != http.StatusForbidden {
t.Fatalf("status not captured: %#v", apiErr.Status)
}
}

// expirationCases covers the three shapes the API can send: a date, an
// explicit null (never expires), and an older server that omits the field.
var expirationCases = []struct {
Expand Down
Loading