wallet-sync-server/server/wallet_test.go
2022-09-19 18:36:55 -04:00

327 lines
11 KiB
Go

package server
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"lbryio/wallet-sync-server/auth"
"lbryio/wallet-sync-server/server/paths"
"lbryio/wallet-sync-server/store"
"lbryio/wallet-sync-server/wallet"
)
func TestServerGetWallet(t *testing.T) {
tt := []struct {
name string
tokenString auth.AuthTokenString
expectedStatusCode int
expectedErrorString string
storeErrors TestStoreFunctionsErrors
}{
{
name: "success",
tokenString: auth.AuthTokenString("seekrit"),
expectedStatusCode: http.StatusOK,
},
{
name: "validation error", // missing auth token
tokenString: auth.AuthTokenString(""),
expectedStatusCode: http.StatusBadRequest,
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Missing token parameter",
// Just check one validation error (missing auth token) to make sure the
// validate function is called. We'll check the rest of the validation
// errors in the other test below.
},
{
name: "auth error",
tokenString: auth.AuthTokenString("seekrit"),
expectedStatusCode: http.StatusUnauthorized,
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found",
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoTokenForUserDevice},
},
{
name: "db error getting wallet",
tokenString: auth.AuthTokenString("seekrit"),
expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError),
storeErrors: TestStoreFunctionsErrors{GetWallet: fmt.Errorf("Some random DB Error!")},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
testAuth := TestAuth{}
testStore := TestStore{
TestAuthToken: auth.AuthToken{
Token: auth.AuthTokenString(tc.tokenString),
Scope: auth.ScopeFull,
},
TestEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
TestSequence: wallet.Sequence(2),
TestHmac: wallet.WalletHmac("my-hmac"),
Errors: tc.storeErrors,
}
testEnv := TestEnv{}
s := Init(&testAuth, &testStore, &testEnv, &TestMail{}, TestPort)
req := httptest.NewRequest(http.MethodGet, paths.PathWallet, nil)
q := req.URL.Query()
q.Add("token", string(testStore.TestAuthToken.Token))
req.URL.RawQuery = q.Encode()
w := httptest.NewRecorder()
// test handleWallet while we're at it, which is a dispatch for get and post
// wallet
s.handleWallet(w, req)
// Make sure we tried to get an auth based on the `token` param (whether or
// not it was a valid `token`)
// NOTE: For tests that set testStore.TestAuthToken.Token=="", this will
// pass even if GetToken isn't called. But we don't care, we expect the
// request to fail for other reasons at that point.
if want, got := testStore.TestAuthToken.Token, testStore.Called.GetToken; want != got {
t.Errorf("testStore.Called.GetToken called with: expected %s, got %s", want, got)
}
body, _ := ioutil.ReadAll(w.Body)
expectStatusCode(t, w, tc.expectedStatusCode)
expectErrorString(t, body, tc.expectedErrorString)
// In this case, a wallet body is expected iff there is no error string
expectWalletBody := len(tc.expectedErrorString) == 0
if !expectWalletBody {
return // The rest of the test does not apply
}
var result WalletResponse
err := json.Unmarshal(body, &result)
if err != nil ||
result.EncryptedWallet != testStore.TestEncryptedWallet ||
result.Hmac != testStore.TestHmac ||
result.Sequence != testStore.TestSequence {
t.Errorf("Expected wallet response to have the test wallet values: result: %+v err: %+v", string(body), err)
}
if !testStore.Called.GetWallet {
t.Errorf("Expected Store.GetWallet to be called")
}
})
}
}
func TestServerPostWallet(t *testing.T) {
tt := []struct {
name string
expectedStatusCode int
expectedErrorString string
expectSetWalletCall bool
expectWsMsg bool
// This is getting messy, but in the case of validation failures, we don't
// even get around to trying to get an auth token, since the token string is
// part of what's being validated. So, we want to be able to skip that
// check in that case.
skipAuthCheck bool
// `new...` refers to what is being passed into the via POST request (and
// what we expect to get passed into SetWallet for the *non-error* cases
// below)
newEncryptedWallet wallet.EncryptedWallet
newSequence wallet.Sequence
newHmac wallet.WalletHmac
storeErrors TestStoreFunctionsErrors
}{
{
name: "success",
expectedStatusCode: http.StatusOK,
expectSetWalletCall: true,
expectWsMsg: true,
// Simulates a situation where the existing sequence is 1, the new
// sequence is 2.
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
newSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
}, {
name: "conflict",
expectedStatusCode: http.StatusConflict,
expectedErrorString: http.StatusText(http.StatusConflict) + ": Bad sequence number",
expectSetWalletCall: true,
// Simulates a situation where the existing sequence is *not* 1, the new
// proposed sequence is 2, and it thus fails with a conflict.
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet-new"),
newSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac-new"),
storeErrors: TestStoreFunctionsErrors{SetWallet: store.ErrWrongSequence},
}, {
name: "validation error",
expectedStatusCode: http.StatusBadRequest,
expectedErrorString: http.StatusText(http.StatusBadRequest) + ": Request failed validation: Missing 'encryptedWallet'",
skipAuthCheck: true, // we can't get an auth token without the data we just failed to validate
// Just check one validation error (empty encrypted wallet) to make sure the
// validate function is called. We'll check the rest of the validation
// errors in the other test below.
newEncryptedWallet: wallet.EncryptedWallet(""),
newSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
}, {
name: "auth error",
expectedStatusCode: http.StatusUnauthorized,
expectedErrorString: http.StatusText(http.StatusUnauthorized) + ": Token Not Found",
// Putting in valid data here so it's clear that this isn't what causes
// the error
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
newSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
// What causes the error
storeErrors: TestStoreFunctionsErrors{GetToken: store.ErrNoTokenForUserDevice},
}, {
name: "db error setting wallet",
expectedStatusCode: http.StatusInternalServerError,
expectedErrorString: http.StatusText(http.StatusInternalServerError),
expectSetWalletCall: true,
// Putting in valid data here so it's clear that this isn't what causes
// the error
newEncryptedWallet: wallet.EncryptedWallet("my-encrypted-wallet"),
newSequence: wallet.Sequence(2),
newHmac: wallet.WalletHmac("my-hmac"),
// What causes the error
storeErrors: TestStoreFunctionsErrors{SetWallet: fmt.Errorf("Some random db problem")},
},
// TODO
// Future test case when we get lastSynced back: Error if
// lastSynced.device_id doesn't match authToken.device_id
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
testAuth := TestAuth{}
testStore := TestStore{
TestAuthToken: auth.AuthToken{
Token: auth.AuthTokenString("seekrit"),
Scope: auth.ScopeFull,
UserId: auth.UserId(37),
},
Errors: tc.storeErrors,
}
s := Init(&testAuth, &testStore, &TestEnv{}, &TestMail{}, TestPort)
wsmm := wsMockManager{s: s, done: make(chan bool)}
requestBody := []byte(
fmt.Sprintf(`{
"token": "%s",
"encryptedWallet": "%s",
"sequence": %d,
"hmac": "%s"
}`, testStore.TestAuthToken.Token, tc.newEncryptedWallet, tc.newSequence, tc.newHmac),
)
req := httptest.NewRequest(http.MethodPost, paths.PathWallet, bytes.NewBuffer(requestBody))
w := httptest.NewRecorder()
// test handleWallet while we're at it, which is a dispatch for get and post
// wallet
go wsmm.getOneMessage(100 * time.Millisecond)
s.handleWallet(w, req)
<-wsmm.done
if tc.expectWsMsg && wsmm.walletUpdateUserId != testStore.TestAuthToken.UserId {
t.Error("Expected websocket message to update wallet")
}
if !tc.expectWsMsg && wsmm.walletUpdateUserId == testStore.TestAuthToken.UserId {
t.Error("Expected no websocket message to update wallet")
}
// Make sure we tried to get an auth based on the `token` param (whether or
// not it was a valid `token`)
if want, got := testStore.TestAuthToken.Token, testStore.Called.GetToken; !tc.skipAuthCheck && want != got {
t.Errorf("testStore.Called.GetToken called with: expected %s, got %s", want, got)
}
body, _ := ioutil.ReadAll(w.Body)
expectStatusCode(t, w, tc.expectedStatusCode)
expectErrorString(t, body, tc.expectedErrorString)
if tc.expectedErrorString == "" && string(body) != "{}" {
t.Errorf("Expected post wallet response to be \"{}\": result: %+v", string(body))
}
if want, got := (SetWalletCall{tc.newEncryptedWallet, tc.newSequence, tc.newHmac}), testStore.Called.SetWallet; tc.expectSetWalletCall && want != got {
t.Errorf("Store.SetWallet called with: expected %+v, got %+v", want, got)
}
})
}
}
func TestServerValidateWalletRequest(t *testing.T) {
walletRequest := WalletRequest{Token: "seekrit", EncryptedWallet: "my-encrypted-wallet", Hmac: "my-hmac", Sequence: 2}
if walletRequest.validate() != nil {
t.Errorf("Expected valid WalletRequest to successfully validate")
}
tt := []struct {
walletRequest WalletRequest
expectedErrorSubstr string
failureDescription string
}{
{
WalletRequest{EncryptedWallet: "my-encrypted-wallet", Hmac: "my-hmac", Sequence: 2},
"token",
"Expected WalletRequest with missing token to not successfully validate",
}, {
WalletRequest{Token: "seekrit", Hmac: "my-hmac", Sequence: 2},
"encryptedWallet",
"Expected WalletRequest with missing encrypted wallet to not successfully validate",
}, {
WalletRequest{Token: "seekrit", EncryptedWallet: "my-encrypted-wallet", Sequence: 2},
"hmac",
"Expected WalletRequest with missing hmac to not successfully validate",
}, {
WalletRequest{Token: "seekrit", EncryptedWallet: "my-encrypted-wallet", Hmac: "my-hmac", Sequence: 0},
"sequence",
"Expected WalletRequest with sequence < 1 to not successfully validate",
},
}
for _, tc := range tt {
err := tc.walletRequest.validate()
if err == nil || !strings.Contains(err.Error(), tc.expectedErrorSubstr) {
t.Errorf(tc.failureDescription)
}
}
}