From f3dca6fad8fec932c7dc83ce6afa6e871c677d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Cidre?= Date: Thu, 29 Mar 2018 11:20:55 +0200 Subject: [PATCH] [#785] /lib specific code living on common moved to /lib --- geth/common/types.go | 63 -- geth/common/utils.go | 25 - lib/library.go | 32 +- lib/library_test.go | 9 +- lib/library_test_utils.go | 1479 +++++++++++++++++++++++++++++++++++++ lib/types.go | 77 ++ lib/utils.go | 1479 +------------------------------------ 7 files changed, 1591 insertions(+), 1573 deletions(-) create mode 100644 lib/library_test_utils.go create mode 100644 lib/types.go diff --git a/geth/common/types.go b/geth/common/types.go index 0720da1cf..0fcc556ed 100644 --- a/geth/common/types.go +++ b/geth/common/types.go @@ -5,9 +5,7 @@ import ( "context" "encoding/json" "errors" - "fmt" "os" - "strings" "time" "github.com/ethereum/go-ethereum/common" @@ -85,67 +83,6 @@ func isNilOrEmpty(bytes hexutil.Bytes) bool { return bytes == nil || len(bytes) == 0 } -// APIResponse generic response from API -type APIResponse struct { - Error string `json:"error"` -} - -// APIDetailedResponse represents a generic response -// with possible errors. -type APIDetailedResponse struct { - Status bool `json:"status"` - Message string `json:"message,omitempty"` - FieldErrors []APIFieldError `json:"field_errors,omitempty"` -} - -func (r APIDetailedResponse) Error() string { - buf := bytes.NewBufferString("") - - for _, err := range r.FieldErrors { - buf.WriteString(err.Error() + "\n") // nolint: gas - } - - return strings.TrimSpace(buf.String()) -} - -// APIFieldError represents a set of errors -// related to a parameter. -type APIFieldError struct { - Parameter string `json:"parameter,omitempty"` - Errors []APIError `json:"errors"` -} - -func (e APIFieldError) Error() string { - if len(e.Errors) == 0 { - return "" - } - - buf := bytes.NewBufferString(fmt.Sprintf("Parameter: %s\n", e.Parameter)) - - for _, err := range e.Errors { - buf.WriteString(err.Error() + "\n") // nolint: gas - } - - return strings.TrimSpace(buf.String()) -} - -// APIError represents a single error. -type APIError struct { - Message string `json:"message"` -} - -func (e APIError) Error() string { - return fmt.Sprintf("message=%s", e.Message) -} - -// AccountInfo represents account's info -type AccountInfo struct { - Address string `json:"address"` - PubKey string `json:"pubkey"` - Mnemonic string `json:"mnemonic"` - Error string `json:"error"` -} - // StopRPCCallError defines a error type specific for killing a execution process. type StopRPCCallError struct { Err error diff --git a/geth/common/utils.go b/geth/common/utils.go index 3dc53bd96..5bab57ef0 100644 --- a/geth/common/utils.go +++ b/geth/common/utils.go @@ -2,7 +2,6 @@ package common import ( "context" - "encoding/json" "fmt" "io" "io/ioutil" @@ -10,7 +9,6 @@ import ( "path/filepath" "reflect" "runtime/debug" - "time" "github.com/ethereum/go-ethereum/log" "github.com/pborman/uuid" @@ -44,18 +42,6 @@ func ImportTestAccount(keystoreDir, accountFile string) error { return err } -// 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) - } - }() -} - // MessageIDFromContext returns message id from context (if exists) func MessageIDFromContext(ctx context.Context) string { if ctx == nil { @@ -68,17 +54,6 @@ func MessageIDFromContext(ctx context.Context) string { return "" } -// ParseJSONArray parses JSON array into Go array of string -func ParseJSONArray(items string) ([]string, error) { - var parsedItems []string - err := json.Unmarshal([]byte(items), &parsedItems) - if err != nil { - return nil, err - } - - return parsedItems, nil -} - // Fatalf is used to halt the execution. // When called the function prints stack end exits. // Failure is logged into both StdErr and StdOut. diff --git a/lib/library.go b/lib/library.go index dfa46c5a5..022697651 100644 --- a/lib/library.go +++ b/lib/library.go @@ -55,22 +55,22 @@ func StopNode() *C.char { //ValidateNodeConfig validates config for status node //export ValidateNodeConfig func ValidateNodeConfig(configJSON *C.char) *C.char { - var resp common.APIDetailedResponse + var resp APIDetailedResponse _, err := params.LoadNodeConfig(C.GoString(configJSON)) - // Convert errors to common.APIDetailedResponse + // Convert errors to APIDetailedResponse switch err := err.(type) { case validator.ValidationErrors: - resp = common.APIDetailedResponse{ + resp = APIDetailedResponse{ Message: "validation: validation failed", - FieldErrors: make([]common.APIFieldError, len(err)), + FieldErrors: make([]APIFieldError, len(err)), } for i, ve := range err { - resp.FieldErrors[i] = common.APIFieldError{ + resp.FieldErrors[i] = APIFieldError{ Parameter: ve.Namespace(), - Errors: []common.APIError{ + Errors: []APIError{ { Message: fmt.Sprintf("field validation failed on the '%s' tag", ve.Tag()), }, @@ -78,11 +78,11 @@ func ValidateNodeConfig(configJSON *C.char) *C.char { } } case error: - resp = common.APIDetailedResponse{ + resp = APIDetailedResponse{ Message: fmt.Sprintf("validation: %s", err.Error()), } case nil: - resp = common.APIDetailedResponse{ + resp = APIDetailedResponse{ Status: true, } } @@ -121,7 +121,7 @@ func CreateAccount(password *C.char) *C.char { errString = err.Error() } - out := common.AccountInfo{ + out := AccountInfo{ Address: address, PubKey: pubKey, Mnemonic: mnemonic, @@ -142,7 +142,7 @@ func CreateChildAccount(parentAddress, password *C.char) *C.char { errString = err.Error() } - out := common.AccountInfo{ + out := AccountInfo{ Address: address, PubKey: pubKey, Error: errString, @@ -162,7 +162,7 @@ func RecoverAccount(password, mnemonic *C.char) *C.char { errString = err.Error() } - out := common.AccountInfo{ + out := AccountInfo{ Address: address, PubKey: pubKey, Mnemonic: C.GoString(mnemonic), @@ -225,7 +225,7 @@ func CompleteTransactions(ids, password *C.char) *C.char { out := common.CompleteTransactionsResult{} out.Results = make(map[string]common.CompleteTransactionResult) - parsedIDs, err := common.ParseJSONArray(C.GoString(ids)) + parsedIDs, err := ParseJSONArray(C.GoString(ids)) if err != nil { out.Results["none"] = common.CompleteTransactionResult{ Error: err.Error(), @@ -288,7 +288,7 @@ func DiscardTransactions(ids *C.char) *C.char { out := common.DiscardTransactionsResult{} out.Results = make(map[string]common.DiscardTransactionResult) - parsedIDs, err := common.ParseJSONArray(C.GoString(ids)) + parsedIDs, err := ParseJSONArray(C.GoString(ids)) if err != nil { out.Results["none"] = common.DiscardTransactionResult{ Error: err.Error(), @@ -383,7 +383,7 @@ func makeJSONResponse(err error) *C.char { errString = err.Error() } - out := common.APIResponse{ + out := APIResponse{ Error: errString, } outBytes, _ := json.Marshal(out) @@ -409,7 +409,7 @@ func NotifyUsers(message, payloadJSON, tokensArray *C.char) (outCBytes *C.char) errString := "" defer func() { - out := common.NotifyResult{ + out := NotifyResult{ Status: err == nil, Error: errString, } @@ -424,7 +424,7 @@ func NotifyUsers(message, payloadJSON, tokensArray *C.char) (outCBytes *C.char) outCBytes = C.CString(string(outBytes)) }() - tokens, err := common.ParseJSONArray(C.GoString(tokensArray)) + tokens, err := ParseJSONArray(C.GoString(tokensArray)) if err != nil { errString = err.Error() return diff --git a/lib/library_test.go b/lib/library_test.go index c2c08062d..9cf048e77 100644 --- a/lib/library_test.go +++ b/lib/library_test.go @@ -9,7 +9,6 @@ package main import ( "testing" - "github.com/status-im/status-go/geth/common" "github.com/stretchr/testify/require" ) @@ -23,7 +22,7 @@ func TestExportedAPI(t *testing.T) { } func TestValidateNodeConfig(t *testing.T) { - noErrorsCallback := func(resp common.APIDetailedResponse) { + noErrorsCallback := func(resp APIDetailedResponse) { require.True(t, resp.Status, "expected status equal true") require.Empty(t, resp.FieldErrors) require.Empty(t, resp.Message) @@ -32,7 +31,7 @@ func TestValidateNodeConfig(t *testing.T) { testCases := []struct { Name string Config string - Callback func(common.APIDetailedResponse) + Callback func(APIDetailedResponse) }{ { Name: "response for valid config", @@ -45,7 +44,7 @@ func TestValidateNodeConfig(t *testing.T) { { Name: "response for invalid JSON string", Config: `{"Network": }`, - Callback: func(resp common.APIDetailedResponse) { + Callback: func(resp APIDetailedResponse) { require.False(t, resp.Status) require.Contains(t, resp.Message, "validation: invalid character '}'") }, @@ -53,7 +52,7 @@ func TestValidateNodeConfig(t *testing.T) { { Name: "response for config with multiple errors", Config: `{}`, - Callback: func(resp common.APIDetailedResponse) { + Callback: func(resp APIDetailedResponse) { required := map[string]string{ "NodeConfig.NetworkID": "required", "NodeConfig.DataDir": "required", diff --git a/lib/library_test_utils.go b/lib/library_test_utils.go new file mode 100644 index 000000000..5bab2f7ff --- /dev/null +++ b/lib/library_test_utils.go @@ -0,0 +1,1479 @@ +// +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/common" + "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/geth/transactions/queue" + "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 := common.ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil { + panic(err) + } + if err := common.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 = common.ImportTestAccount(tmpDir, GetAccount1PKFile()); err != nil { + t.Fatal(err) + } + if err = common.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.NodeManager()) + 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.NodeManager().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.NodeManager().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.NodeManager().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.NodeManager().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.NodeManager().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.NodeManager().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.NodeManager().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 { + txQueueManager := statusAPI.TxQueueManager() + txQueue := txQueueManager.TransactionQueue() + + txQueue.Reset() + EnsureNodeSync(statusAPI.NodeManager()) + + // 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 == transactions.EventTransactionQueued { + event := envelope.Event.(map[string]interface{}) + t.Logf("transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string)) + + completeTxResponse := common.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(), common.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 txQueue.Count() != 0 { + t.Error("tx queue must be empty at this point") + return false + } + + return true +} + +func testCompleteMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocyclo + txQueue := statusAPI.TxQueueManager().TransactionQueue() + txQueue.Reset() + + // 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 == transactions.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(), common.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 := common.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 != queue.ErrQueuedTxIDNotFound.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 txQueue.Has(common.QueuedTxID(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 txQueue.Count() != 0 { + t.Error("tx queue must be empty at this point") + return false + } + + return true +} + +func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo + txQueue := statusAPI.TxQueueManager().TransactionQueue() + txQueue.Reset() + + // 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 == transactions.EventTransactionQueued { + event := envelope.Event.(map[string]interface{}) + txID = event["id"].(string) + t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID) + + if !txQueue.Has(common.QueuedTxID(txID)) { + t.Errorf("txqueue should still have test tx: %s", txID) + return + } + + // discard + discardResponse := common.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(common.QueuedTxID(txID), TestConfig.Account1.Password) + if err != queue.ErrQueuedTxIDNotFound { + t.Error("expects tx not found, but call to CompleteTransaction succeeded") + return + } + + time.Sleep(1 * time.Second) // make sure that tx complete signal propagates + if txQueue.Has(common.QueuedTxID(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 == transactions.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 := transactions.ErrQueuedTxDiscarded.Error() + if receivedErrMessage != expectedErrMessage { + t.Errorf("unexpected error message received: got %v", receivedErrMessage) + return + } + + receivedErrCode := event["error_code"].(string) + if receivedErrCode != strconv.Itoa(transactions.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(), common.SendTxArgs{ + From: account.FromAddress(TestConfig.Account1.Address), + To: account.ToAddress(TestConfig.Account2.Address), + Value: (*hexutil.Big)(big.NewInt(1000000000000)), + }) + if err != transactions.ErrQueuedTxDiscarded { + 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 txQueue.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 + txQueue := statusAPI.TxQueueManager().TransactionQueue() + txQueue.Reset() + + // 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 == transactions.EventTransactionQueued { + event := envelope.Event.(map[string]interface{}) + txID = event["id"].(string) + t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID) + + if !txQueue.Has(common.QueuedTxID(txID)) { + t.Errorf("txqueue should still have test tx: %s", txID) + return + } + + txIDs <- txID + } + + if envelope.Type == transactions.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 := transactions.ErrQueuedTxDiscarded.Error() + if receivedErrMessage != expectedErrMessage { + t.Errorf("unexpected error message received: got %v", receivedErrMessage) + return + } + + receivedErrCode := event["error_code"].(string) + if receivedErrCode != strconv.Itoa(transactions.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(), common.SendTxArgs{ + From: account.FromAddress(TestConfig.Account1.Address), + To: account.ToAddress(TestConfig.Account2.Address), + Value: (*hexutil.Big)(big.NewInt(1000000000000)), + }) + if err != transactions.ErrQueuedTxDiscarded { + 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 := common.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 != queue.ErrQueuedTxIDNotFound.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 := common.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 != queue.ErrQueuedTxIDNotFound.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 txQueue.Has(common.QueuedTxID(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 txQueue.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 := common.ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil { + panic(err) + } + if err := common.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 == transactions.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.NodeManager()) + } 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) +} diff --git a/lib/types.go b/lib/types.go new file mode 100644 index 000000000..b8bd9fdf6 --- /dev/null +++ b/lib/types.go @@ -0,0 +1,77 @@ +package main + +import ( + "bytes" + "fmt" + "strings" +) + +// APIResponse generic response from API. +type APIResponse struct { + Error string `json:"error"` +} + +// APIDetailedResponse represents a generic response +// with possible errors. +type APIDetailedResponse struct { + Status bool `json:"status"` + Message string `json:"message,omitempty"` + FieldErrors []APIFieldError `json:"field_errors,omitempty"` +} + +// Error string representation of APIDetailedResponse. +func (r APIDetailedResponse) Error() string { + buf := bytes.NewBufferString("") + + for _, err := range r.FieldErrors { + buf.WriteString(err.Error() + "\n") // nolint: gas + } + + return strings.TrimSpace(buf.String()) +} + +// APIFieldError represents a set of errors +// related to a parameter. +type APIFieldError struct { + Parameter string `json:"parameter,omitempty"` + Errors []APIError `json:"errors"` +} + +// Error string representation of APIFieldError. +func (e APIFieldError) Error() string { + if len(e.Errors) == 0 { + return "" + } + + buf := bytes.NewBufferString(fmt.Sprintf("Parameter: %s\n", e.Parameter)) + + for _, err := range e.Errors { + buf.WriteString(err.Error() + "\n") // nolint: gas + } + + return strings.TrimSpace(buf.String()) +} + +// APIError represents a single error. +type APIError struct { + Message string `json:"message"` +} + +// Error string representation of APIError. +func (e APIError) Error() string { + return fmt.Sprintf("message=%s", e.Message) +} + +// AccountInfo represents account's info. +type AccountInfo struct { + Address string `json:"address"` + PubKey string `json:"pubkey"` + Mnemonic string `json:"mnemonic"` + Error string `json:"error"` +} + +// NotifyResult is a JSON returned from notify message. +type NotifyResult struct { + Status bool `json:"status"` + Error string `json:"error,omitempty"` +} diff --git a/lib/utils.go b/lib/utils.go index ce1288e3f..819772d27 100644 --- a/lib/utils.go +++ b/lib/utils.go @@ -1,1479 +1,30 @@ -// +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/common" - "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/geth/transactions/queue" - "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 := common.ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil { - panic(err) - } - if err := common.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 = common.ImportTestAccount(tmpDir, GetAccount1PKFile()); err != nil { - t.Fatal(err) - } - if err = common.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 := common.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 := common.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.NodeManager()) - 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.NodeManager().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 := common.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 := common.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 := common.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.NodeManager().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.NodeManager().AccountKeyStore() - if err != nil { - t.Error(err) - return false - } - - // create an account - createAccountResponse := common.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 := common.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 = common.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 := common.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 := common.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 := common.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.NodeManager().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 := common.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 = common.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 = common.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.NodeManager().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.NodeManager().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 := common.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 = common.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 = common.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.NodeManager().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 := common.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 { - txQueueManager := statusAPI.TxQueueManager() - txQueue := txQueueManager.TransactionQueue() - - txQueue.Reset() - EnsureNodeSync(statusAPI.NodeManager()) - - // 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) - common.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 == transactions.EventTransactionQueued { - event := envelope.Event.(map[string]interface{}) - t.Logf("transaction queued (will be completed shortly): {id: %s}\n", event["id"].(string)) - - completeTxResponse := common.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(), common.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 txQueue.Count() != 0 { - t.Error("tx queue must be empty at this point") - return false - } - - return true -} - -func testCompleteMultipleQueuedTransactions(t *testing.T) bool { //nolint: gocyclo - txQueue := statusAPI.TxQueueManager().TransactionQueue() - txQueue.Reset() - - // 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 == transactions.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(), common.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 := common.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 != queue.ErrQueuedTxIDNotFound.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 txQueue.Has(common.QueuedTxID(txID)) { - t.Errorf("txqueue should not have test tx at this point (it should be completed): %s", txID) - return - } - } - } +// PanicAfter throws panic() after waitSeconds, unless abort channel receives +// notification. +func PanicAfter(waitSeconds time.Duration, abort chan struct{}, desc string) { 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 txQueue.Count() != 0 { - t.Error("tx queue must be empty at this point") - return false - } - - return true -} - -func testDiscardTransaction(t *testing.T) bool { //nolint: gocyclo - txQueue := statusAPI.TxQueueManager().TransactionQueue() - txQueue.Reset() - - // 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) - common.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) + select { + case <-abort: return - } - if envelope.Type == transactions.EventTransactionQueued { - event := envelope.Event.(map[string]interface{}) - txID = event["id"].(string) - t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID) - - if !txQueue.Has(common.QueuedTxID(txID)) { - t.Errorf("txqueue should still have test tx: %s", txID) - return - } - - // discard - discardResponse := common.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(common.QueuedTxID(txID), TestConfig.Account1.Password) - if err != queue.ErrQueuedTxIDNotFound { - t.Error("expects tx not found, but call to CompleteTransaction succeeded") - return - } - - time.Sleep(1 * time.Second) // make sure that tx complete signal propagates - if txQueue.Has(common.QueuedTxID(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 == transactions.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 := transactions.ErrQueuedTxDiscarded.Error() - if receivedErrMessage != expectedErrMessage { - t.Errorf("unexpected error message received: got %v", receivedErrMessage) - return - } - - receivedErrCode := event["error_code"].(string) - if receivedErrCode != strconv.Itoa(transactions.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(), common.SendTxArgs{ - From: account.FromAddress(TestConfig.Account1.Address), - To: account.ToAddress(TestConfig.Account2.Address), - Value: (*hexutil.Big)(big.NewInt(1000000000000)), - }) - if err != transactions.ErrQueuedTxDiscarded { - 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 txQueue.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 - txQueue := statusAPI.TxQueueManager().TransactionQueue() - txQueue.Reset() - - // 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 == transactions.EventTransactionQueued { - event := envelope.Event.(map[string]interface{}) - txID = event["id"].(string) - t.Logf("transaction queued (will be discarded soon): {id: %s}\n", txID) - - if !txQueue.Has(common.QueuedTxID(txID)) { - t.Errorf("txqueue should still have test tx: %s", txID) - return - } - - txIDs <- txID - } - - if envelope.Type == transactions.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 := transactions.ErrQueuedTxDiscarded.Error() - if receivedErrMessage != expectedErrMessage { - t.Errorf("unexpected error message received: got %v", receivedErrMessage) - return - } - - receivedErrCode := event["error_code"].(string) - if receivedErrCode != strconv.Itoa(transactions.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(), common.SendTxArgs{ - From: account.FromAddress(TestConfig.Account1.Address), - To: account.ToAddress(TestConfig.Account2.Address), - Value: (*hexutil.Big)(big.NewInt(1000000000000)), - }) - if err != transactions.ErrQueuedTxDiscarded { - 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 := common.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 != queue.ErrQueuedTxIDNotFound.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 := common.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 != queue.ErrQueuedTxIDNotFound.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 txQueue.Has(common.QueuedTxID(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 txQueue.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 := common.ImportTestAccount(testKeyDir, GetAccount1PKFile()); err != nil { - panic(err) - } - if err := common.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 == transactions.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.NodeManager()) - } else { - time.Sleep(5 * time.Second) - } - - // now we can proceed with tests - waitForNodeStart <- struct{}{} - } - }) - - go func() { - response := StartNode(C.CString(nodeConfigJSON)) - responseErr := common.APIResponse{} - - if err := json.Unmarshal([]byte(C.GoString(response)), &responseErr); err != nil { - panic(err) - } - if responseErr.Error != "" { - panic("cannot start node: " + responseErr.Error) + case <-time.After(waitSeconds): + panic("whatever you were doing takes toooo long: " + desc) } }() - - return waitForNodeStart } -//nolint: deadcode -func testValidateNodeConfig(t *testing.T, config string, fn func(common.APIDetailedResponse)) { - result := ValidateNodeConfig(C.CString(config)) +// ParseJSONArray parses JSON array into Go array of string. +func ParseJSONArray(items string) ([]string, error) { + var parsedItems []string + err := json.Unmarshal([]byte(items), &parsedItems) + if err != nil { + return nil, err + } - var resp common.APIDetailedResponse - - err := json.Unmarshal([]byte(C.GoString(result)), &resp) - require.NoError(t, err) - - fn(resp) + return parsedItems, nil }