status-go/lib/library_test_utils.go
Igor Mandrigin a9eb5a7d2b Generalize signing requests.
We need to be able to sign more than just transactions to make DApps
work properly. This change separates signing requests from the
transactions and make it more general to prepare to intoduce different
types of signing requests.

This change is designed to preserve status APIs, so it is
backward-comparible with the current API bindings.
2018-04-09 20:48:00 +02:00

1487 lines
46 KiB
Go

// +build e2e_test
// This is a file with e2e tests for C bindings written in library.go.
// As a CGO file, it can't have `_test.go` suffix as it's not allowed by Go.
// At the same time, we don't want this file to be included in the binaries.
// This is why `e2e_test` tag was introduced. Without it, this file is excluded
// from the build. Providing this tag will include this file into the build
// and that's what is done while running e2e tests for `lib/` package.
package main
import "C"
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"testing"
"time"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
gethparams "github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
"github.com/status-im/status-go/geth/account"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/geth/signal"
"github.com/status-im/status-go/geth/transactions"
"github.com/status-im/status-go/sign"
"github.com/status-im/status-go/static"
. "github.com/status-im/status-go/t/utils" //nolint: golint
)
const zeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000"
const initJS = `
var _status_catalog = {
foo: 'bar'
};`
var testChainDir string
var nodeConfigJSON string
func init() {
testChainDir = filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
nodeConfigJSON = `{
"NetworkId": ` + strconv.Itoa(GetNetworkID()) + `,
"DataDir": "` + testChainDir + `",
"HTTPPort": ` + strconv.Itoa(TestConfig.Node.HTTPPort) + `,
"WSPort": ` + strconv.Itoa(TestConfig.Node.WSPort) + `,
"LogLevel": "INFO"
}`
}
// nolint: deadcode
func testExportedAPI(t *testing.T, done chan struct{}) {
<-startTestNode(t)
defer func() {
done <- struct{}{}
}()
// prepare accounts
testKeyDir := filepath.Join(testChainDir, "keystore")
if err := ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil {
panic(err)
}
if err := ImportTestAccount(testKeyDir, GetAccount2PKFile()); err != nil {
panic(err)
}
// FIXME(tiabc): All of that is done because usage of cgo is not supported in tests.
// Probably, there should be a cleaner way, for example, test cgo bindings in e2e tests
// separately from other internal tests.
// FIXME(@jekamas): ATTENTION! this tests depends on each other!
tests := []struct {
name string
fn func(t *testing.T) bool
}{
{
"check default configuration",
testGetDefaultConfig,
},
{
"stop/resume node",
testStopResumeNode,
},
{
"call RPC on in-proc handler",
testCallRPC,
},
{
"create main and child accounts",
testCreateChildAccount,
},
{
"verify account password",
testVerifyAccountPassword,
},
{
"recover account",
testRecoverAccount,
},
{
"account select/login",
testAccountSelect,
},
{
"account logout",
testAccountLogout,
},
{
"complete single queued transaction",
testCompleteTransaction,
},
{
"test complete multiple queued transactions",
testCompleteMultipleQueuedTransactions,
},
{
"discard single queued transaction",
testDiscardTransaction,
},
{
"test discard multiple queued transactions",
testDiscardMultipleQueuedTransactions,
},
{
"test jail invalid initialization",
testJailInitInvalid,
},
{
"test jail invalid parse",
testJailParseInvalid,
},
{
"test jail initialization",
testJailInit,
},
{
"test jailed calls",
testJailFunctionCall,
},
{
"test ExecuteJS",
testExecuteJS,
},
{
"test deprecated Parse",
testJailParseDeprecated,
},
}
for _, test := range tests {
t.Logf("=== RUN %s", test.name)
if ok := test.fn(t); !ok {
t.Logf("=== FAILED %s", test.name)
break
}
}
}
func testVerifyAccountPassword(t *testing.T) bool {
tmpDir, err := ioutil.TempDir(os.TempDir(), "accounts")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) // nolint: errcheck
if err = ImportTestAccount(tmpDir, GetAccount1PKFile()); err != nil {
t.Fatal(err)
}
if err = ImportTestAccount(tmpDir, GetAccount2PKFile()); err != nil {
t.Fatal(err)
}
// rename account file (to see that file's internals reviewed, when locating account key)
accountFilePathOriginal := filepath.Join(tmpDir, GetAccount1PKFile())
accountFilePath := filepath.Join(tmpDir, "foo"+TestConfig.Account1.Address+"bar.pk")
if err := os.Rename(accountFilePathOriginal, accountFilePath); err != nil {
t.Fatal(err)
}
response := APIResponse{}
rawResponse := VerifyAccountPassword(
C.CString(tmpDir),
C.CString(TestConfig.Account1.Address),
C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
t.Errorf("cannot decode response (%s): %v", C.GoString(rawResponse), err)
return false
}
if response.Error != "" {
t.Errorf("unexpected error: %s", response.Error)
return false
}
return true
}
func testGetDefaultConfig(t *testing.T) bool {
networks := []struct {
chainID int
refChainConfig *gethparams.ChainConfig
}{
{params.MainNetworkID, gethparams.MainnetChainConfig},
{params.RopstenNetworkID, gethparams.TestnetChainConfig},
{params.RinkebyNetworkID, gethparams.RinkebyChainConfig},
// TODO(tiabc): The same for params.StatusChainNetworkID
}
for i := range networks {
network := networks[i]
t.Run(fmt.Sprintf("networkID=%d", network.chainID), func(t *testing.T) {
var (
nodeConfig = params.NodeConfig{}
rawResponse = GenerateConfig(C.CString("/tmp/data-folder"), C.int(network.chainID), 1)
)
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &nodeConfig); err != nil {
t.Errorf("cannot decode response (%s): %v", C.GoString(rawResponse), err)
}
genesis := new(core.Genesis)
if err := json.Unmarshal([]byte(nodeConfig.LightEthConfig.Genesis), genesis); err != nil {
t.Error(err)
}
require.Equal(t, network.refChainConfig, genesis.Config)
})
}
return true
}
//@TODO(adam): quarantined this test until it uses a different directory.
//nolint: deadcode
func testResetChainData(t *testing.T) bool {
t.Skip()
resetChainDataResponse := APIResponse{}
rawResponse := ResetChainData()
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &resetChainDataResponse); err != nil {
t.Errorf("cannot decode ResetChainData response (%s): %v", C.GoString(rawResponse), err)
return false
}
if resetChainDataResponse.Error != "" {
t.Errorf("unexpected error: %s", resetChainDataResponse.Error)
return false
}
EnsureNodeSync(statusAPI.StatusNode().EnsureSync)
testCompleteTransaction(t)
return true
}
func testStopResumeNode(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusAPI.Logout(); err != nil {
t.Fatal(err)
}
whisperService, err := statusAPI.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create an account
address1, pubKey1, _, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
}
t.Logf("account created: {address: %s, key: %s}", address1, pubKey1)
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity already present in whisper")
}
// select account
loginResponse := APIResponse{}
rawResponse := Login(C.CString(address1), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error != "" {
t.Errorf("could not select account: %v", err)
return false
}
if !whisperService.HasKeyPair(pubKey1) {
t.Errorf("identity not injected into whisper: %v", err)
}
// stop and resume node, then make sure that selected account is still selected
// nolint: dupl
stopNodeFn := func() bool {
response := APIResponse{}
// FIXME(tiabc): Implement https://github.com/status-im/status-go/issues/254 to avoid
// 9-sec timeout below after stopping the node.
rawResponse = StopNode()
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
t.Errorf("cannot decode StopNode response (%s): %v", C.GoString(rawResponse), err)
return false
}
if response.Error != "" {
t.Errorf("unexpected error: %s", response.Error)
return false
}
return true
}
// nolint: dupl
resumeNodeFn := func() bool {
response := APIResponse{}
// FIXME(tiabc): Implement https://github.com/status-im/status-go/issues/254 to avoid
// 10-sec timeout below after resuming the node.
rawResponse = StartNode(C.CString(nodeConfigJSON))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &response); err != nil {
t.Errorf("cannot decode StartNode response (%s): %v", C.GoString(rawResponse), err)
return false
}
if response.Error != "" {
t.Errorf("unexpected error: %s", response.Error)
return false
}
return true
}
if !stopNodeFn() {
return false
}
time.Sleep(9 * time.Second) // allow to stop
if !resumeNodeFn() {
return false
}
time.Sleep(10 * time.Second) // allow to start (instead of using blocking version of start, of filter event)
// now, verify that we still have account logged in
whisperService, err = statusAPI.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
if !whisperService.HasKeyPair(pubKey1) {
t.Errorf("identity evicted from whisper on node restart: %v", err)
}
// additionally, let's complete transaction (just to make sure that node lives through pause/resume w/o issues)
testCompleteTransaction(t)
return true
}
func testCallRPC(t *testing.T) bool {
expected := `{"jsonrpc":"2.0","id":64,"result":"0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad"}`
rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"web3_sha3","params":["0x68656c6c6f20776f726c64"],"id":64}`))
received := C.GoString(rawResponse)
if expected != received {
t.Errorf("unexpected response: expected: %v, got: %v", expected, received)
return false
}
return true
}
func testCreateChildAccount(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might get populated during previous tests)
if err := statusAPI.Logout(); err != nil {
t.Fatal(err)
}
keyStore, err := statusAPI.StatusNode().AccountKeyStore()
if err != nil {
t.Error(err)
return false
}
// create an account
createAccountResponse := AccountInfo{}
rawResponse := CreateAccount(C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &createAccountResponse); err != nil {
t.Errorf("cannot decode CreateAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if createAccountResponse.Error != "" {
t.Errorf("could not create account: %s", err)
return false
}
address, pubKey, mnemonic := createAccountResponse.Address, createAccountResponse.PubKey, createAccountResponse.Mnemonic
t.Logf("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
acct, err := account.ParseAccountString(address)
if err != nil {
t.Errorf("can not get account from address: %v", err)
return false
}
// obtain decrypted key, and make sure that extended key (which will be used as root for sub-accounts) is present
_, key, err := keyStore.AccountDecryptedKey(acct, TestConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return false
}
if key.ExtendedKey == nil {
t.Error("CKD#2 has not been generated for new account")
return false
}
// try creating sub-account, w/o selecting main account i.e. w/o login to main account
createSubAccountResponse := AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if createSubAccountResponse.Error != account.ErrNoAccountSelected.Error() {
t.Errorf("expected error is not returned (tried to create sub-account w/o login): %v", createSubAccountResponse.Error)
return false
}
err = statusAPI.SelectAccount(address, TestConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return false
}
// try to create sub-account with wrong password
createSubAccountResponse = AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString("wrong password"))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if createSubAccountResponse.Error != "cannot retrieve a valid key for a given account: could not decrypt key with given passphrase" {
t.Errorf("expected error is not returned (tried to create sub-account with wrong password): %v", createSubAccountResponse.Error)
return false
}
// create sub-account (from implicit parent)
createSubAccountResponse1 := AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse1); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if createSubAccountResponse1.Error != "" {
t.Errorf("cannot create sub-account: %v", createSubAccountResponse1.Error)
return false
}
// make sure that sub-account index automatically progresses
createSubAccountResponse2 := AccountInfo{}
rawResponse = CreateChildAccount(C.CString(""), C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse2); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if createSubAccountResponse2.Error != "" {
t.Errorf("cannot create sub-account: %v", createSubAccountResponse2.Error)
}
if createSubAccountResponse1.Address == createSubAccountResponse2.Address || createSubAccountResponse1.PubKey == createSubAccountResponse2.PubKey {
t.Error("sub-account index auto-increament failed")
return false
}
// create sub-account (from explicit parent)
createSubAccountResponse3 := AccountInfo{}
rawResponse = CreateChildAccount(C.CString(createSubAccountResponse2.Address), C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &createSubAccountResponse3); err != nil {
t.Errorf("cannot decode CreateChildAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if createSubAccountResponse3.Error != "" {
t.Errorf("cannot create sub-account: %v", createSubAccountResponse3.Error)
}
subAccount1, subAccount2, subAccount3 := createSubAccountResponse1.Address, createSubAccountResponse2.Address, createSubAccountResponse3.Address
subPubKey1, subPubKey2, subPubKey3 := createSubAccountResponse1.PubKey, createSubAccountResponse2.PubKey, createSubAccountResponse3.PubKey
if subAccount1 == subAccount3 || subPubKey1 == subPubKey3 || subAccount2 == subAccount3 || subPubKey2 == subPubKey3 {
t.Error("sub-account index auto-increament failed")
return false
}
return true
}
func testRecoverAccount(t *testing.T) bool { //nolint: gocyclo
keyStore, _ := statusAPI.StatusNode().AccountKeyStore()
// create an account
address, pubKey, mnemonic, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
}
t.Logf("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic)
// try recovering using password + mnemonic
recoverAccountResponse := AccountInfo{}
rawResponse := RecoverAccount(C.CString(TestConfig.Account1.Password), C.CString(mnemonic))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &recoverAccountResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if recoverAccountResponse.Error != "" {
t.Errorf("recover account failed: %v", recoverAccountResponse.Error)
return false
}
addressCheck, pubKeyCheck := recoverAccountResponse.Address, recoverAccountResponse.PubKey
if address != addressCheck || pubKey != pubKeyCheck {
t.Error("recover account details failed to pull the correct details")
}
// now test recovering, but make sure that account/key file is removed i.e. simulate recovering on a new device
account, err := account.ParseAccountString(address)
if err != nil {
t.Errorf("can not get account from address: %v", err)
}
account, key, err := keyStore.AccountDecryptedKey(account, TestConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return false
}
extChild2String := key.ExtendedKey.String()
if err = keyStore.Delete(account, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot remove account: %v", err)
}
recoverAccountResponse = AccountInfo{}
rawResponse = RecoverAccount(C.CString(TestConfig.Account1.Password), C.CString(mnemonic))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &recoverAccountResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if recoverAccountResponse.Error != "" {
t.Errorf("recover account failed (for non-cached account): %v", recoverAccountResponse.Error)
return false
}
addressCheck, pubKeyCheck = recoverAccountResponse.Address, recoverAccountResponse.PubKey
if address != addressCheck || pubKey != pubKeyCheck {
t.Error("recover account details failed to pull the correct details (for non-cached account)")
}
// make sure that extended key exists and is imported ok too
_, key, err = keyStore.AccountDecryptedKey(account, TestConfig.Account1.Password)
if err != nil {
t.Errorf("can not obtain decrypted account key: %v", err)
return false
}
if extChild2String != key.ExtendedKey.String() {
t.Errorf("CKD#2 key mismatch, expected: %s, got: %s", extChild2String, key.ExtendedKey.String())
}
// make sure that calling import several times, just returns from cache (no error is expected)
recoverAccountResponse = AccountInfo{}
rawResponse = RecoverAccount(C.CString(TestConfig.Account1.Password), C.CString(mnemonic))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &recoverAccountResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if recoverAccountResponse.Error != "" {
t.Errorf("recover account failed (for non-cached account): %v", recoverAccountResponse.Error)
return false
}
addressCheck, pubKeyCheck = recoverAccountResponse.Address, recoverAccountResponse.PubKey
if address != addressCheck || pubKey != pubKeyCheck {
t.Error("recover account details failed to pull the correct details (for non-cached account)")
}
// time to login with recovered data
whisperService, err := statusAPI.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(pubKeyCheck) {
t.Error("identity already present in whisper")
}
err = statusAPI.SelectAccount(addressCheck, TestConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return false
}
if !whisperService.HasKeyPair(pubKeyCheck) {
t.Errorf("identity not injected into whisper: %v", err)
}
return true
}
func testAccountSelect(t *testing.T) bool { //nolint: gocyclo
// test to see if the account was injected in whisper
whisperService, err := statusAPI.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
// create an account
address1, pubKey1, _, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
}
t.Logf("Account created: {address: %s, key: %s}", address1, pubKey1)
address2, pubKey2, _, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Error("Test failed: could not create account")
return false
}
t.Logf("Account created: {address: %s, key: %s}", address2, pubKey2)
// make sure that identity is not (yet injected)
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity already present in whisper")
}
// try selecting with wrong password
loginResponse := APIResponse{}
rawResponse := Login(C.CString(address1), C.CString("wrongPassword"))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error == "" {
t.Error("select account is expected to throw error: wrong password used")
return false
}
loginResponse = APIResponse{}
rawResponse = Login(C.CString(address1), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error != "" {
t.Errorf("Test failed: could not select account: %v", err)
return false
}
if !whisperService.HasKeyPair(pubKey1) {
t.Errorf("identity not injected into whisper: %v", err)
}
// select another account, make sure that previous account is wiped out from Whisper cache
if whisperService.HasKeyPair(pubKey2) {
t.Error("identity already present in whisper")
}
loginResponse = APIResponse{}
rawResponse = Login(C.CString(address2), C.CString(TestConfig.Account1.Password))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error != "" {
t.Errorf("Test failed: could not select account: %v", loginResponse.Error)
return false
}
if !whisperService.HasKeyPair(pubKey2) {
t.Errorf("identity not injected into whisper: %v", err)
}
if whisperService.HasKeyPair(pubKey1) {
t.Error("identity should be removed, but it is still present in whisper")
}
return true
}
func testAccountLogout(t *testing.T) bool {
whisperService, err := statusAPI.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
return false
}
// create an account
address, pubKey, _, err := statusAPI.CreateAccount(TestConfig.Account1.Password)
if err != nil {
t.Errorf("could not create account: %v", err)
return false
}
// make sure that identity doesn't exist (yet) in Whisper
if whisperService.HasKeyPair(pubKey) {
t.Error("identity already present in whisper")
return false
}
// select/login
err = statusAPI.SelectAccount(address, TestConfig.Account1.Password)
if err != nil {
t.Errorf("Test failed: could not select account: %v", err)
return false
}
if !whisperService.HasKeyPair(pubKey) {
t.Error("identity not injected into whisper")
return false
}
logoutResponse := APIResponse{}
rawResponse := Logout()
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &logoutResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
return false
}
if logoutResponse.Error != "" {
t.Errorf("cannot logout: %v", logoutResponse.Error)
return false
}
// now, logout and check if identity is removed indeed
if whisperService.HasKeyPair(pubKey) {
t.Error("identity not cleared from whisper")
return false
}
return true
}
func testCompleteTransaction(t *testing.T) bool {
signRequests := statusAPI.PendingSignRequests()
EnsureNodeSync(statusAPI.StatusNode().EnsureSync)
// log into account from which transactions will be sent
if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v. Error %q", TestConfig.Account1.Address, err)
return false
}
// make sure you panic if transaction complete doesn't return
queuedTxCompleted := make(chan struct{}, 1)
abortPanic := make(chan struct{}, 1)
PanicAfter(10*time.Second, abortPanic, "testCompleteTransaction")
// replace transaction notification handler
var txHash = ""
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope signal.Envelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s. Error %q", jsonEvent, err)
return
}
if envelope.Type == sign.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string))
completeTxResponse := CompleteTransactionResult{}
rawResponse := CompleteTransaction(C.CString(event["id"].(string)), C.CString(TestConfig.Account1.Password))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &completeTxResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
}
if completeTxResponse.Error != "" {
t.Errorf("cannot complete queued transaction[%v]: %v", event["id"], completeTxResponse.Error)
}
txHash = completeTxResponse.Hash
t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txHash)
abortPanic <- struct{}{} // so that timeout is aborted
queuedTxCompleted <- struct{}{}
}
})
// this call blocks, up until Complete Transaction is called
txCheckHash, err := statusAPI.SendTransaction(context.TODO(), transactions.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
t.Errorf("Failed to SendTransaction: %s", err)
return false
}
<-queuedTxCompleted // make sure that complete transaction handler completes its magic, before we proceed
if txHash != txCheckHash.Hex() {
t.Errorf("Transaction hash returned from SendTransaction is invalid: expected %s, got %s",
txCheckHash.Hex(), txHash)
return false
}
if reflect.DeepEqual(txCheckHash, gethcommon.Hash{}) {
t.Error("Test failed: transaction was never queued or completed")
return false
}
if signRequests.Count() != 0 {
t.Error("tx queue must be empty at this point")
return false
}
return true
}
func testCompleteMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocyclo
signRequests := statusAPI.PendingSignRequests()
// log into account from which transactions will be sent
if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
return false
}
// make sure you panic if transaction complete doesn't return
testTxCount := 3
txIDs := make(chan string, testTxCount)
allTestTxCompleted := make(chan struct{}, 1)
// replace transaction notification handler
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var txID string
var envelope signal.Envelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == sign.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be completed in a single call, once aggregated): {id: %s}\n", txID)
txIDs <- txID
}
})
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
sendTx := func() {
txHashCheck, err := statusAPI.SendTransaction(context.TODO(), transactions.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
t.Errorf("unexpected error thrown: %v", err)
return
}
if reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
t.Error("transaction returned empty hash")
return
}
}
// wait for transactions, and complete them in a single call
completeTxs := func(txIDStrings string) {
var parsedIDs []string
if err := json.Unmarshal([]byte(txIDStrings), &parsedIDs); err != nil {
t.Error(err)
return
}
parsedIDs = append(parsedIDs, "invalid-tx-id")
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
// complete
resultsString := CompleteTransactions(C.CString(string(updatedTxIDStrings)), C.CString(TestConfig.Account1.Password))
resultsStruct := CompleteTransactionsResult{}
if err := json.Unmarshal([]byte(C.GoString(resultsString)), &resultsStruct); err != nil {
t.Error(err)
return
}
results := resultsStruct.Results
if len(results) != (testTxCount+1) || results["invalid-tx-id"].Error != sign.ErrSignReqNotFound.Error() {
t.Errorf("cannot complete txs: %v", results)
return
}
for txID, txResult := range results {
if txID != txResult.ID {
t.Errorf("tx id not set in result: expected id is %s", txID)
return
}
if txResult.Error != "" && txID != "invalid-tx-id" {
t.Errorf("invalid error for %s", txID)
return
}
if txResult.Hash == zeroHash && txID != "invalid-tx-id" {
t.Errorf("invalid hash (expected non empty hash): %s", txID)
return
}
if txResult.Hash != zeroHash {
t.Logf("transaction complete: https://testnet.etherscan.io/tx/%s", txResult.Hash)
}
}
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
for _, txID := range parsedIDs {
if signRequests.Has(string(txID)) {
t.Errorf("txqueue should not have test tx at this point (it should be completed): %s", txID)
return
}
}
}
go func() {
var txIDStrings []string
for i := 0; i < testTxCount; i++ {
txIDStrings = append(txIDStrings, <-txIDs)
}
txIDJSON, _ := json.Marshal(txIDStrings)
completeTxs(string(txIDJSON))
allTestTxCompleted <- struct{}{}
}()
// send multiple transactions
for i := 0; i < testTxCount; i++ {
go sendTx()
}
select {
case <-allTestTxCompleted:
// pass
case <-time.After(20 * time.Second):
t.Error("test timed out")
return false
}
if signRequests.Count() != 0 {
t.Error("tx queue must be empty at this point")
return false
}
return true
}
func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo
signRequests := statusAPI.PendingSignRequests()
// log into account from which transactions will be sent
if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
return false
}
// make sure you panic if transaction complete doesn't return
completeQueuedTransaction := make(chan struct{}, 1)
PanicAfter(20*time.Second, completeQueuedTransaction, "testDiscardTransaction")
// replace transaction notification handler
var txID string
txFailedEventCalled := false
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope signal.Envelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == sign.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID)
if !signRequests.Has(string(txID)) {
t.Errorf("txqueue should still have test tx: %s", txID)
return
}
// discard
discardResponse := DiscardTransactionResult{}
rawResponse := DiscardTransaction(C.CString(txID))
if err := json.Unmarshal([]byte(C.GoString(rawResponse)), &discardResponse); err != nil {
t.Errorf("cannot decode RecoverAccount response (%s): %v", C.GoString(rawResponse), err)
}
if discardResponse.Error != "" {
t.Errorf("cannot discard tx: %v", discardResponse.Error)
return
}
// try completing discarded transaction
_, err := statusAPI.CompleteTransaction(string(txID), TestConfig.Account1.Password)
if err != sign.ErrSignReqNotFound {
t.Error("expects tx not found, but call to CompleteTransaction succeeded")
return
}
if signRequests.Has(string(txID)) {
t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txID)
return
}
completeQueuedTransaction <- struct{}{} // so that timeout is aborted
}
if envelope.Type == sign.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction return event received: {id: %s}\n", event["id"].(string))
receivedErrMessage := event["error_message"].(string)
expectedErrMessage := sign.ErrSignReqDiscarded.Error()
if receivedErrMessage != expectedErrMessage {
t.Errorf("unexpected error message received: got %v", receivedErrMessage)
return
}
receivedErrCode := event["error_code"].(string)
if receivedErrCode != strconv.Itoa(sign.SendTransactionDiscardedErrorCode) {
t.Errorf("unexpected error code received: got %v", receivedErrCode)
return
}
txFailedEventCalled = true
}
})
// this call blocks, and should return when DiscardQueuedTransaction() is called
txHashCheck, err := statusAPI.SendTransaction(context.TODO(), transactions.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
time.Sleep(1 * time.Second)
if err != sign.ErrSignReqDiscarded {
t.Errorf("expected error not thrown: %v", err)
return false
}
if !reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
t.Error("transaction returned hash, while it shouldn't")
return false
}
if signRequests.Count() != 0 {
t.Error("tx queue must be empty at this point")
return false
}
if !txFailedEventCalled {
t.Error("expected tx failure signal is not received")
return false
}
return true
}
func testDiscardMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocyclo
signRequests := statusAPI.PendingSignRequests()
// log into account from which transactions will be sent
if err := statusAPI.SelectAccount(TestConfig.Account1.Address, TestConfig.Account1.Password); err != nil {
t.Errorf("cannot select account: %v", TestConfig.Account1.Address)
return false
}
// make sure you panic if transaction complete doesn't return
testTxCount := 3
txIDs := make(chan string, testTxCount)
allTestTxDiscarded := make(chan struct{}, 1)
// replace transaction notification handler
txFailedEventCallCount := 0
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var txID string
var envelope signal.Envelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == sign.EventTransactionQueued {
event := envelope.Event.(map[string]interface{})
txID = event["id"].(string)
t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID)
if !signRequests.Has(string(txID)) {
t.Errorf("txqueue should still have test tx: %s", txID)
return
}
txIDs <- txID
}
if envelope.Type == sign.EventTransactionFailed {
event := envelope.Event.(map[string]interface{})
t.Logf("transaction return event received: {id: %s}\n", event["id"].(string))
receivedErrMessage := event["error_message"].(string)
expectedErrMessage := sign.ErrSignReqDiscarded.Error()
if receivedErrMessage != expectedErrMessage {
t.Errorf("unexpected error message received: got %v", receivedErrMessage)
return
}
receivedErrCode := event["error_code"].(string)
if receivedErrCode != strconv.Itoa(sign.SendTransactionDiscardedErrorCode) {
t.Errorf("unexpected error code received: got %v", receivedErrCode)
return
}
txFailedEventCallCount++
if txFailedEventCallCount == testTxCount {
allTestTxDiscarded <- struct{}{}
}
}
})
// this call blocks, and should return when DiscardQueuedTransaction() for a given tx id is called
sendTx := func() {
txHashCheck, err := statusAPI.SendTransaction(context.TODO(), transactions.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.Address),
To: account.ToAddress(TestConfig.Account2.Address),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != sign.ErrSignReqDiscarded {
t.Errorf("expected error not thrown: %v", err)
return
}
if !reflect.DeepEqual(txHashCheck, gethcommon.Hash{}) {
t.Error("transaction returned hash, while it shouldn't")
return
}
}
// wait for transactions, and discard immediately
discardTxs := func(txIDStrings string) {
var parsedIDs []string
if err := json.Unmarshal([]byte(txIDStrings), &parsedIDs); err != nil {
t.Error(err)
return
}
parsedIDs = append(parsedIDs, "invalid-tx-id")
updatedTxIDStrings, _ := json.Marshal(parsedIDs)
// discard
discardResultsString := DiscardTransactions(C.CString(string(updatedTxIDStrings)))
discardResultsStruct := DiscardTransactionsResult{}
if err := json.Unmarshal([]byte(C.GoString(discardResultsString)), &discardResultsStruct); err != nil {
t.Error(err)
return
}
discardResults := discardResultsStruct.Results
if len(discardResults) != 1 || discardResults["invalid-tx-id"].Error != sign.ErrSignReqNotFound.Error() {
t.Errorf("cannot discard txs: %v", discardResults)
return
}
// try completing discarded transaction
completeResultsString := CompleteTransactions(C.CString(string(updatedTxIDStrings)), C.CString(TestConfig.Account1.Password))
completeResultsStruct := CompleteTransactionsResult{}
if err := json.Unmarshal([]byte(C.GoString(completeResultsString)), &completeResultsStruct); err != nil {
t.Error(err)
return
}
completeResults := completeResultsStruct.Results
if len(completeResults) != (testTxCount + 1) {
t.Error("unexpected number of errors (call to CompleteTransaction should not succeed)")
}
for txID, txResult := range completeResults {
if txID != txResult.ID {
t.Errorf("tx id not set in result: expected id is %s", txID)
return
}
if txResult.Error != sign.ErrSignReqNotFound.Error() {
t.Errorf("invalid error for %s", txResult.Hash)
return
}
if txResult.Hash != zeroHash {
t.Errorf("invalid hash (expected zero): %s", txResult.Hash)
return
}
}
time.Sleep(1 * time.Second) // make sure that tx complete signal propagates
for _, txID := range parsedIDs {
if signRequests.Has(string(txID)) {
t.Errorf("txqueue should not have test tx at this point (it should be discarded): %s", txID)
return
}
}
}
go func() {
var txIDStrings []string
for i := 0; i < testTxCount; i++ {
txIDStrings = append(txIDStrings, <-txIDs)
}
txIDJSON, _ := json.Marshal(txIDStrings)
discardTxs(string(txIDJSON))
}()
// send multiple transactions
for i := 0; i < testTxCount; i++ {
go sendTx()
}
select {
case <-allTestTxDiscarded:
// pass
case <-time.After(20 * time.Second):
t.Error("test timed out")
return false
}
if signRequests.Count() != 0 {
t.Error("tx queue must be empty at this point")
return false
}
return true
}
func testJailInitInvalid(t *testing.T) bool {
// Arrange.
initInvalidCode := `
var _status_catalog = {
foo: 'bar'
`
// Act.
InitJail(C.CString(initInvalidCode))
response := C.GoString(CreateAndInitCell(C.CString("CHAT_ID_INIT_INVALID_TEST"), C.CString(``)))
// Assert.
expectedSubstr := `"error":"(anonymous): Line 4:3 Unexpected identifier`
if !strings.Contains(response, expectedSubstr) {
t.Errorf("unexpected response, didn't find '%s' in '%s'", expectedSubstr, response)
return false
}
return true
}
func testJailParseInvalid(t *testing.T) bool {
// Arrange.
InitJail(C.CString(initJS))
// Act.
extraInvalidCode := `
var extraFunc = function (x) {
return x * x;
`
response := C.GoString(CreateAndInitCell(C.CString("CHAT_ID_PARSE_INVALID_TEST"), C.CString(extraInvalidCode)))
// Assert.
expectedResponse := `{"error":"(anonymous): Line 4:2 Unexpected end of input (and 1 more errors)"}`
if expectedResponse != response {
t.Errorf("unexpected response, expected: %v, got: %v", expectedResponse, response)
return false
}
return true
}
func testJailInit(t *testing.T) bool {
InitJail(C.CString(initJS))
chatID := C.CString("CHAT_ID_INIT_TEST")
// Cell initialization return the result of the last JS operation provided to it.
response := CreateAndInitCell(chatID, C.CString(`var extraFunc = function (x) { return x * x; }; extraFunc(2);`))
require.Equal(t, `{"result":4}`, C.GoString(response), "Unexpected response from jail.CreateAndInitCell()")
// Commands from the jail initialization are available in any of the created cells.
response = ExecuteJS(chatID, C.CString(`JSON.stringify({ result: _status_catalog });`))
require.Equal(t, `{"result":{"foo":"bar"}}`, C.GoString(response), "Environment from `InitJail` is not available in the created cell")
return true
}
func testJailParseDeprecated(t *testing.T) bool {
InitJail(C.CString(initJS))
extraCode := `
var extraFunc = function (x) {
return x * x;
};
extraFunc(2);
`
chatID := C.CString("CHAT_ID_PARSE_TEST")
response := Parse(chatID, C.CString(extraCode))
require.Equal(t, `{"result":4}`, C.GoString(response))
// cell already exists but Parse should not complain
response = Parse(chatID, C.CString(extraCode))
require.Equal(t, `{"result":4}`, C.GoString(response))
// test extraCode
response = ExecuteJS(chatID, C.CString(`extraFunc(10)`))
require.Equal(t, `100`, C.GoString(response))
return true
}
func testJailFunctionCall(t *testing.T) bool {
InitJail(C.CString(""))
// load Status JS and add test command to it
statusJS := string(static.MustAsset("testdata/jail/status.js")) + `;
_status_catalog.commands["testCommand"] = function (params) {
return params.val * params.val;
};`
CreateAndInitCell(C.CString("CHAT_ID_CALL_TEST"), C.CString(statusJS))
// call with wrong chat id
rawResponse := Call(C.CString("CHAT_IDNON_EXISTENT"), C.CString(""), C.CString(""))
parsedResponse := C.GoString(rawResponse)
expectedError := `{"error":"cell 'CHAT_IDNON_EXISTENT' not found"}`
if parsedResponse != expectedError {
t.Errorf("expected error is not returned: expected %s, got %s", expectedError, parsedResponse)
return false
}
// call extraFunc()
rawResponse = Call(C.CString("CHAT_ID_CALL_TEST"), C.CString(`["commands", "testCommand"]`), C.CString(`{"val": 12}`))
parsedResponse = C.GoString(rawResponse)
expectedResponse := `{"result":144}`
if parsedResponse != expectedResponse {
t.Errorf("expected response is not returned: expected %s, got %s", expectedResponse, parsedResponse)
return false
}
t.Logf("jailed method called: %s", parsedResponse)
return true
}
func testExecuteJS(t *testing.T) bool {
InitJail(C.CString(""))
// cell does not exist
response := C.GoString(ExecuteJS(C.CString("CHAT_ID_EXECUTE_TEST"), C.CString("('some string')")))
expectedResponse := `{"error":"cell 'CHAT_ID_EXECUTE_TEST' not found"}`
if response != expectedResponse {
t.Errorf("expected '%s' but got '%s'", expectedResponse, response)
return false
}
CreateAndInitCell(C.CString("CHAT_ID_EXECUTE_TEST"), C.CString(`var obj = { status: true }`))
// cell does not exist
response = C.GoString(ExecuteJS(C.CString("CHAT_ID_EXECUTE_TEST"), C.CString(`JSON.stringify(obj)`)))
expectedResponse = `{"status":true}`
if response != expectedResponse {
t.Errorf("expected '%s' but got '%s'", expectedResponse, response)
return false
}
return true
}
func startTestNode(t *testing.T) <-chan struct{} {
testDir := filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
syncRequired := false
if _, err := os.Stat(testDir); os.IsNotExist(err) {
syncRequired = true
}
// inject test accounts
testKeyDir := filepath.Join(testDir, "keystore")
if err := ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil {
panic(err)
}
if err := ImportTestAccount(testKeyDir, GetAccount2PKFile()); err != nil {
panic(err)
}
waitForNodeStart := make(chan struct{}, 1)
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
t.Log(jsonEvent)
var envelope signal.Envelope
if err := json.Unmarshal([]byte(jsonEvent), &envelope); err != nil {
t.Errorf("cannot unmarshal event's JSON: %s", jsonEvent)
return
}
if envelope.Type == signal.EventNodeCrashed {
signal.TriggerDefaultNodeNotificationHandler(jsonEvent)
return
}
if envelope.Type == sign.EventTransactionQueued {
}
if envelope.Type == signal.EventNodeStarted {
t.Log("Node started, but we wait till it be ready")
}
if envelope.Type == signal.EventNodeReady {
// sync
if syncRequired {
t.Logf("Sync is required")
EnsureNodeSync(statusAPI.StatusNode().EnsureSync)
} else {
time.Sleep(5 * time.Second)
}
// now we can proceed with tests
waitForNodeStart <- struct{}{}
}
})
go func() {
response := StartNode(C.CString(nodeConfigJSON))
responseErr := APIResponse{}
if err := json.Unmarshal([]byte(C.GoString(response)), &responseErr); err != nil {
panic(err)
}
if responseErr.Error != "" {
panic("cannot start node: " + responseErr.Error)
}
}()
return waitForNodeStart
}
//nolint: deadcode
func testValidateNodeConfig(t *testing.T, config string, fn func(APIDetailedResponse)) {
result := ValidateNodeConfig(C.CString(config))
var resp APIDetailedResponse
err := json.Unmarshal([]byte(C.GoString(result)), &resp)
require.NoError(t, err)
fn(resp)
}
// PanicAfter throws panic() after waitSeconds, unless abort channel receives
// notification.
func PanicAfter(waitSeconds time.Duration, abort chan struct{}, desc string) {
go func() {
select {
case <-abort:
return
case <-time.After(waitSeconds):
panic("whatever you were doing takes toooo long: " + desc)
}
}()
}