diff --git a/client/client.go b/client/client.go index 29948fb..529ce2d 100644 --- a/client/client.go +++ b/client/client.go @@ -206,8 +206,11 @@ func (c *Client) do(ctx context.Context, method, path string, addr stdaddr.Addre } // Try to unmarshal the response body to a known vspd error. + d := json.NewDecoder(bytes.NewReader(respBody)) + d.DisallowUnknownFields() + var apiError types.ErrorResponse - err = json.Unmarshal(respBody, &apiError) + err = d.Decode(&apiError) if err == nil { return apiError } diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000..8543caa --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,102 @@ +package client + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/decred/dcrd/txscript/v4/stdaddr" + "github.com/decred/slog" + "github.com/decred/vspd/types" +) + +func NoopSign(ctx context.Context, message string, address stdaddr.Address) ([]byte, error) { + return nil, nil +} + +func NoopValidate(resp *http.Response, body []byte, serverPubkey []byte) error { + return nil +} + +// TestErrorDetails ensures errors returned by client.do contain adequate +// information for debugging (HTTP status and response body). +func TestErrorDetails(t *testing.T) { + + tests := map[string]struct { + httpStatus int + responseBodyBytes []byte + expectedErr string + vspdError bool + }{ + "500, vspd error": { + httpStatus: 500, + responseBodyBytes: []byte(`{"code": 1, "message": "bad request"}`), + expectedErr: `bad request`, + vspdError: true, + }, + "500, no body": { + httpStatus: 500, + responseBodyBytes: nil, + expectedErr: `http status 500 (Internal Server Error) with no body`, + vspdError: false, + }, + "500, non vspd error": { + httpStatus: 500, + responseBodyBytes: []byte(`an error occurred`), + expectedErr: `http status 500 (Internal Server Error) with body "an error occurred"`, + vspdError: false, + }, + "500, non vspd error (json)": { + httpStatus: 500, + responseBodyBytes: []byte(`{"some": "json"}`), + expectedErr: `http status 500 (Internal Server Error) with body "{\"some\": \"json\"}"`, + vspdError: false, + }, + } + + for testName, testData := range tests { + t.Run(testName, func(t *testing.T) { + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + res.WriteHeader(testData.httpStatus) + _, err := res.Write(testData.responseBodyBytes) + if err != nil { + t.Fatalf("writing response body failed: %v", err) + } + })) + + client := Client{ + URL: testServer.URL, + PubKey: []byte("fake pubkey"), + Sign: NoopSign, + Validate: NoopValidate, + Log: slog.Disabled, + } + + var resp interface{} + err := client.do(context.TODO(), http.MethodGet, "", nil, &resp, nil) + + testServer.Close() + + if err == nil { + t.Fatalf("client.do did not return an error") + } + + if err.Error() != testData.expectedErr { + t.Fatalf("client.do returned incorrect error, expected %q, got %q", + testData.expectedErr, err.Error()) + } + + if testData.vspdError { + // Error should be unwrappable as a vspd error response. + var e types.ErrorResponse + if !errors.As(err, &e) { + t.Fatal("unable to unwrap vspd error") + } + } + + }) + } +}