Upcoming changes constitute breaking public API changes to both the client and types modules, therefore this bumps the version numbers of both modules and adds local replacements to go.mod files such that the new versions can be used before they are publicly tagged.
193 lines
5.3 KiB
Go
193 lines
5.3 KiB
Go
// Copyright (c) 2022-2024 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package client
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/decred/slog"
|
|
"github.com/decred/vspd/types/v3"
|
|
)
|
|
|
|
// 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 {
|
|
respHTTPStatus int
|
|
respBodyBytes []byte
|
|
expectedErr string
|
|
vspdError bool
|
|
vspdErrCode types.ErrorCode
|
|
}{
|
|
"500, vspd error (generic bad request)": {
|
|
respHTTPStatus: 500,
|
|
respBodyBytes: []byte(`{"code": 0, "message": "bad request"}`),
|
|
expectedErr: `bad request`,
|
|
vspdError: true,
|
|
vspdErrCode: types.ErrBadRequest,
|
|
},
|
|
"500, vspd error (generic internal error)": {
|
|
respHTTPStatus: 500,
|
|
respBodyBytes: []byte(`{"code": 1, "message": "something terrible happened"}`),
|
|
expectedErr: `something terrible happened`,
|
|
vspdError: true,
|
|
vspdErrCode: types.ErrInternalError,
|
|
},
|
|
"428, vspd error (cannot broadcast fee)": {
|
|
respHTTPStatus: 428,
|
|
respBodyBytes: []byte(`{"code": 16, "message": "fee transaction could not be broadcast due to unknown outputs"}`),
|
|
expectedErr: `fee transaction could not be broadcast due to unknown outputs`,
|
|
vspdError: true,
|
|
vspdErrCode: types.ErrCannotBroadcastFeeUnknownOutputs,
|
|
},
|
|
"500, no body": {
|
|
respHTTPStatus: 500,
|
|
respBodyBytes: nil,
|
|
expectedErr: `http status 500 (Internal Server Error) with no body`,
|
|
vspdError: false,
|
|
},
|
|
"500, non vspd error": {
|
|
respHTTPStatus: 500,
|
|
respBodyBytes: []byte(`an error occurred`),
|
|
expectedErr: `http status 500 (Internal Server Error) with body "an error occurred"`,
|
|
vspdError: false,
|
|
},
|
|
"500, non vspd error (json)": {
|
|
respHTTPStatus: 500,
|
|
respBodyBytes: []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, _ *http.Request) {
|
|
res.WriteHeader(testData.respHTTPStatus)
|
|
_, err := res.Write(testData.respBodyBytes)
|
|
if err != nil {
|
|
t.Fatalf("writing response body failed: %v", err)
|
|
}
|
|
}))
|
|
|
|
client := Client{
|
|
URL: testServer.URL,
|
|
PubKey: []byte("fake pubkey"),
|
|
Log: slog.Disabled,
|
|
}
|
|
|
|
var resp any
|
|
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")
|
|
}
|
|
|
|
if e.Code != testData.vspdErrCode {
|
|
t.Fatalf("incorrect vspd error code, expected %d, got %d",
|
|
testData.vspdErrCode, e.Code)
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSignatureValidation ensures that responses with invalid signatures are
|
|
// flagged.
|
|
func TestSignatureValidation(t *testing.T) {
|
|
|
|
// Generate some test data for the valid signature case.
|
|
privKey := ed25519.NewKeyFromSeed([]byte("00000000000000000000000000000000"))
|
|
pubKey, _ := privKey.Public().(ed25519.PublicKey)
|
|
emptyJSON := []byte("{}")
|
|
validSig := base64.StdEncoding.EncodeToString(ed25519.Sign(privKey, emptyJSON))
|
|
|
|
tests := map[string]struct {
|
|
responseSig string
|
|
expectErr bool
|
|
expectErrStr string
|
|
}{
|
|
"valid signature": {
|
|
responseSig: validSig,
|
|
expectErr: false,
|
|
},
|
|
"invalid signature": {
|
|
responseSig: "1234",
|
|
expectErr: true,
|
|
expectErrStr: "authenticate server response: invalid signature",
|
|
},
|
|
"no signature": {
|
|
responseSig: "",
|
|
expectErr: true,
|
|
expectErrStr: "authenticate server response: no signature provided",
|
|
},
|
|
"failed to decode signature": {
|
|
responseSig: "0xp",
|
|
expectErr: true,
|
|
expectErrStr: "authenticate server response: failed to decode signature: illegal base64 data at input byte 0",
|
|
},
|
|
}
|
|
|
|
for testName, testData := range tests {
|
|
t.Run(testName, func(t *testing.T) {
|
|
|
|
testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) {
|
|
res.Header().Add("VSP-Server-Signature", testData.responseSig)
|
|
res.WriteHeader(http.StatusOK)
|
|
_, err := res.Write(emptyJSON)
|
|
if err != nil {
|
|
t.Fatalf("writing response body failed: %v", err)
|
|
}
|
|
}))
|
|
|
|
client := Client{
|
|
URL: testServer.URL,
|
|
PubKey: pubKey,
|
|
Log: slog.Disabled,
|
|
}
|
|
|
|
var resp any
|
|
err := client.do(context.TODO(), http.MethodGet, "", nil, &resp, nil)
|
|
|
|
testServer.Close()
|
|
|
|
if testData.expectErr {
|
|
if err.Error() != testData.expectErrStr {
|
|
t.Fatalf("client.do returned incorrect error, expected %q, got %q",
|
|
testData.expectErrStr, err.Error())
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("client.do returned unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|