326 lines
8.6 KiB
Go
326 lines
8.6 KiB
Go
// Copyright (c) 2021-2023 The Decred developers
|
|
// Use of this source code is governed by an ISC
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package webapi
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/decred/dcrd/chaincfg/v3"
|
|
dcrdtypes "github.com/decred/dcrd/rpc/jsonrpc/types/v4"
|
|
"github.com/decred/slog"
|
|
"github.com/decred/vspd/database"
|
|
"github.com/decred/vspd/types/v2"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const (
|
|
// hexCharset is a list of all valid hexadecimal characters.
|
|
hexCharset = "1234567890abcdef"
|
|
// sigCharset is a list of all valid request/response signature characters
|
|
// (base64 encoding).
|
|
sigCharset = "0123456789ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/="
|
|
testDb = "test.db"
|
|
)
|
|
|
|
var (
|
|
seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
feeXPub = "feexpub"
|
|
maxVoteChangeRecords = 3
|
|
api *Server
|
|
)
|
|
|
|
// randBytes returns a byte slice of size n filled with random bytes.
|
|
func randBytes(n int) []byte {
|
|
slice := make([]byte, n)
|
|
if _, err := seededRand.Read(slice); err != nil {
|
|
panic(err)
|
|
}
|
|
return slice
|
|
}
|
|
|
|
func stdoutLogger() slog.Logger {
|
|
backend := slog.NewBackend(os.Stdout)
|
|
log := backend.Logger("test")
|
|
log.SetLevel(slog.LevelTrace)
|
|
return log
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
log := stdoutLogger()
|
|
|
|
// Set up some global params.
|
|
cfg := Config{
|
|
NetParams: chaincfg.MainNetParams(),
|
|
}
|
|
_, signPrivKey, _ := ed25519.GenerateKey(seededRand)
|
|
|
|
// Create a database to use.
|
|
// Ensure we are starting with a clean environment.
|
|
os.Remove(testDb)
|
|
|
|
// Create a new blank database for all tests.
|
|
err := database.CreateNew(testDb, feeXPub, log)
|
|
if err != nil {
|
|
panic(fmt.Errorf("error creating test database: %w", err))
|
|
}
|
|
|
|
// Open the newly created database so it is ready to use.
|
|
db, err := database.Open(testDb, log, maxVoteChangeRecords)
|
|
if err != nil {
|
|
panic(fmt.Errorf("error opening test database: %w", err))
|
|
}
|
|
|
|
api = &Server{
|
|
cfg: cfg,
|
|
signPrivKey: signPrivKey,
|
|
db: db,
|
|
log: log,
|
|
}
|
|
|
|
// Run tests.
|
|
exitCode := m.Run()
|
|
|
|
writeBackup := false
|
|
db.Close(writeBackup)
|
|
os.Remove(testDb)
|
|
|
|
os.Exit(exitCode)
|
|
}
|
|
|
|
// randString randomly generates a string of the requested length, using only
|
|
// characters from the provided charset.
|
|
func randString(length int, charset string) string {
|
|
b := make([]byte, length)
|
|
for i := range b {
|
|
b[i] = charset[seededRand.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// Ensure that testNode satisfies Node.
|
|
var _ Node = (*testNode)(nil)
|
|
|
|
type testNode struct {
|
|
getRawTransaction *dcrdtypes.TxRawResult
|
|
getRawTransactionErr error
|
|
existsLiveTicket bool
|
|
existsLiveTicketErr error
|
|
}
|
|
|
|
func (n *testNode) ExistsLiveTicket(ticketHash string) (bool, error) {
|
|
return n.existsLiveTicket, n.existsLiveTicketErr
|
|
}
|
|
|
|
func (n *testNode) GetRawTransaction(txHash string) (*dcrdtypes.TxRawResult, error) {
|
|
return n.getRawTransaction, n.getRawTransactionErr
|
|
}
|
|
|
|
func TestSetAltSignAddress(t *testing.T) {
|
|
const testAddr = "DsVoDXNQqyF3V83PJJ5zMdnB4pQuJHBAh15"
|
|
tests := map[string]struct {
|
|
dcrdClientErr bool
|
|
deformReq int
|
|
addr string
|
|
node *testNode
|
|
isExistingAltSignAddr bool
|
|
wantHTTPStatus int
|
|
// wantErrCode and wantErrMsg only checked if wantHTTPStatus != 200.
|
|
wantErrCode types.ErrorCode
|
|
wantErrMsg string
|
|
}{
|
|
"ok": {
|
|
addr: testAddr,
|
|
node: &testNode{
|
|
getRawTransaction: &dcrdtypes.TxRawResult{
|
|
Confirmations: 1000,
|
|
},
|
|
getRawTransactionErr: nil,
|
|
existsLiveTicket: true,
|
|
},
|
|
wantHTTPStatus: http.StatusOK,
|
|
},
|
|
"dcrd client error": {
|
|
dcrdClientErr: true,
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
|
wantErrCode: types.ErrInternalError,
|
|
wantErrMsg: types.ErrInternalError.DefaultMessage(),
|
|
},
|
|
"bad request": {
|
|
deformReq: 1,
|
|
wantHTTPStatus: http.StatusBadRequest,
|
|
wantErrCode: types.ErrBadRequest,
|
|
wantErrMsg: "json: cannot unmarshal string into Go value of type types.SetAltSignAddrRequest",
|
|
},
|
|
"bad addr": {
|
|
addr: "xxx",
|
|
wantHTTPStatus: http.StatusBadRequest,
|
|
wantErrCode: types.ErrBadRequest,
|
|
wantErrMsg: "failed to decoded address \"xxx\": invalid format: version and/or checksum bytes missing",
|
|
},
|
|
"addr wrong type": {
|
|
addr: "DkM3ZigNyiwHrsXRjkDQ8t8tW6uKGW9g61qEkG3bMqQPQWYEf5X3J",
|
|
wantHTTPStatus: http.StatusBadRequest,
|
|
wantErrCode: types.ErrBadRequest,
|
|
wantErrMsg: "wrong type for alternate signing address",
|
|
},
|
|
"getRawTransaction error from dcrd client": {
|
|
addr: testAddr,
|
|
node: &testNode{
|
|
getRawTransactionErr: errors.New("getRawTransaction error"),
|
|
},
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
|
wantErrCode: types.ErrInternalError,
|
|
wantErrMsg: types.ErrInternalError.DefaultMessage(),
|
|
},
|
|
"existsLiveTicket error from dcrd client": {
|
|
addr: testAddr,
|
|
node: &testNode{
|
|
getRawTransaction: &dcrdtypes.TxRawResult{
|
|
Confirmations: 1000,
|
|
},
|
|
existsLiveTicketErr: errors.New("existsLiveTicket error"),
|
|
},
|
|
wantHTTPStatus: http.StatusInternalServerError,
|
|
wantErrCode: types.ErrInternalError,
|
|
wantErrMsg: types.ErrInternalError.DefaultMessage(),
|
|
},
|
|
"ticket can't vote": {
|
|
addr: testAddr,
|
|
node: &testNode{
|
|
getRawTransaction: &dcrdtypes.TxRawResult{
|
|
Confirmations: 1000,
|
|
},
|
|
existsLiveTicket: false,
|
|
},
|
|
wantHTTPStatus: http.StatusBadRequest,
|
|
wantErrCode: types.ErrTicketCannotVote,
|
|
wantErrMsg: types.ErrTicketCannotVote.DefaultMessage(),
|
|
},
|
|
"only one alt sign addr allowed": {
|
|
addr: testAddr,
|
|
node: &testNode{
|
|
getRawTransaction: &dcrdtypes.TxRawResult{},
|
|
existsLiveTicket: true,
|
|
},
|
|
isExistingAltSignAddr: true,
|
|
wantHTTPStatus: http.StatusBadRequest,
|
|
wantErrCode: types.ErrBadRequest,
|
|
wantErrMsg: "alternate sign address data already exists",
|
|
},
|
|
}
|
|
|
|
for testName, test := range tests {
|
|
t.Run(testName, func(t *testing.T) {
|
|
ticketHash := randString(64, hexCharset)
|
|
req := &types.SetAltSignAddrRequest{
|
|
Timestamp: time.Now().Unix(),
|
|
TicketHash: ticketHash,
|
|
TicketHex: randString(504, hexCharset),
|
|
ParentHex: randString(504, hexCharset),
|
|
AltSignAddress: test.addr,
|
|
}
|
|
reqSig := randString(504, hexCharset)
|
|
b, err := json.Marshal(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if test.isExistingAltSignAddr {
|
|
data := &database.AltSignAddrData{
|
|
AltSignAddr: test.addr,
|
|
Req: string(b),
|
|
ReqSig: reqSig,
|
|
Resp: string(randBytes(1000)),
|
|
RespSig: randString(96, sigCharset),
|
|
}
|
|
if err := api.db.InsertAltSignAddr(ticketHash, data); err != nil {
|
|
t.Fatalf("unable to insert alt sign addr: %v", err)
|
|
}
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
c, r := gin.CreateTestContext(w)
|
|
|
|
var dcrdErr error
|
|
if test.dcrdClientErr {
|
|
dcrdErr = errors.New("error")
|
|
}
|
|
|
|
handle := func(c *gin.Context) {
|
|
c.Set(dcrdKey, test.node)
|
|
c.Set(dcrdErrorKey, dcrdErr)
|
|
c.Set(requestBytesKey, b[test.deformReq:])
|
|
api.setAltSignAddr(c)
|
|
}
|
|
|
|
r.POST("/", handle)
|
|
|
|
c.Request, err = http.NewRequest(http.MethodPost, "/", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c.Request.Header.Set("VSP-Client-Signature", reqSig)
|
|
|
|
r.ServeHTTP(w, c.Request)
|
|
|
|
if test.wantHTTPStatus != w.Code {
|
|
t.Fatalf("expected http status %d, got %d", test.wantHTTPStatus, w.Code)
|
|
}
|
|
|
|
if test.wantHTTPStatus != http.StatusOK {
|
|
respBytes, err := io.ReadAll(w.Body)
|
|
if err != nil {
|
|
t.Fatalf("failed reading response body bytes: %v", err)
|
|
}
|
|
|
|
var apiError types.ErrorResponse
|
|
err = json.Unmarshal(respBytes, &apiError)
|
|
if err != nil {
|
|
t.Fatalf("could not unmarshal error response: %v", err)
|
|
}
|
|
|
|
if test.wantErrCode != apiError.Code {
|
|
t.Fatalf("incorrect error code, expected %d, actual %d",
|
|
test.wantErrCode, apiError.Code)
|
|
}
|
|
|
|
if test.wantErrMsg != apiError.Message {
|
|
t.Fatalf("incorrect error message, expected %q, actual %q",
|
|
test.wantErrMsg, apiError.Message)
|
|
}
|
|
}
|
|
|
|
altsig, err := api.db.AltSignAddrData(ticketHash)
|
|
if err != nil {
|
|
t.Fatalf("unable to get alt sign addr data: %v", err)
|
|
}
|
|
|
|
if test.wantHTTPStatus != http.StatusOK && !test.isExistingAltSignAddr {
|
|
if altsig != nil {
|
|
t.Fatalf("expected no alt sign addr saved for errored state")
|
|
}
|
|
return
|
|
}
|
|
|
|
if !bytes.Equal(b, []byte(altsig.Req)) || altsig.ReqSig != reqSig {
|
|
t.Fatalf("expected alt sign addr data different than actual")
|
|
}
|
|
})
|
|
}
|
|
}
|