status-go/lib/library_test_utils.go
Roman Volosovskyi 4c0d8dedea
Replace address with keyUid in accounts db
Account's address was used as a primary key in accounts db and as a
deterministic id of an account in some API calls. Also it was used as a
part of the name of the account specific database. This revealed some
extra information about the account and wasn't necessary.
At first the hash of the address was planned to be used as a
deterministic id, but we already have a keyUid which is calculated as
sha256 hash of account's public key and has similar properties:
- it is deterministic
- doesn't reveal accounts public key or address in plain
2019-12-09 11:20:12 +02:00

641 lines
21 KiB
Go

// +build e2e_test
// This is a file with e2e tests for C bindings written in library.go.
// As a CGO file, it can't have `_test.go` suffix as it's not allowed by Go.
// At the same time, we don't want this file to be included in the binaries.
// This is why `e2e_test` tag was introduced. Without it, this file is excluded
// from the build. Providing this tag will include this file into the build
// and that's what is done while running e2e tests for `lib/` package.
package main
import (
"C"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/event"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/signal"
. "github.com/status-im/status-go/t/utils" //nolint: golint
"github.com/status-im/status-go/transactions"
"github.com/stretchr/testify/require"
)
const initJS = `
var _status_catalog = {
foo: 'bar'
};`
var (
zeroHash = common.Hash{}
testChainDir string
keystoreDir string
nodeConfigJSON string
)
func buildAccountData(name, chatAddress string) *C.char {
return C.CString(fmt.Sprintf(`{
"name": "%s",
"key-uid": "%s"
}`, name, chatAddress))
}
func buildSubAccountData(chatAddress string) *C.char {
accs := []accounts.Account{
{
Wallet: true,
Chat: true,
Address: common.HexToAddress(chatAddress),
},
}
data, _ := json.Marshal(accs)
return C.CString(string(data))
}
func buildLoginParams(mainAccountAddress, chatAddress, password string) account.LoginParams {
return account.LoginParams{
ChatAddress: common.HexToAddress(chatAddress),
Password: password,
MainAccount: common.HexToAddress(mainAccountAddress),
}
}
func waitSignal(feed *event.Feed, event string, timeout time.Duration) error {
events := make(chan signal.Envelope)
sub := feed.Subscribe(events)
defer sub.Unsubscribe()
after := time.After(timeout)
for {
select {
case envelope := <-events:
if envelope.Type == event {
return nil
}
case <-after:
return fmt.Errorf("signal %v wasn't received in %v", event, timeout)
}
}
}
func createAccountAndLogin(t *testing.T, feed *event.Feed) account.Info {
account1, _, err := statusBackend.AccountManager().CreateAccount(TestConfig.Account1.Password)
require.NoError(t, err)
t.Logf("account created: {address: %s, key: %s}", account1.WalletAddress, account1.WalletPubKey)
// select account
loginResponse := APIResponse{}
rawResponse := SaveAccountAndLogin(buildAccountData("test", account1.WalletAddress), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(account1.WalletAddress))
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
require.Empty(t, loginResponse.Error)
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
return account1
}
// nolint: deadcode
func testExportedAPI(t *testing.T) {
testDir := filepath.Join(TestDataDir, TestNetworkNames[GetNetworkID()])
_ = OpenAccounts(C.CString(testDir))
// inject test accounts
testKeyDir := filepath.Join(testDir, "keystore")
_ = InitKeystore(C.CString(testKeyDir))
require.NoError(t, ImportTestAccount(testKeyDir, GetAccount1PKFile()))
require.NoError(t, ImportTestAccount(testKeyDir, GetAccount2PKFile()))
// 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.
// NOTE(dshulyak) tests are using same backend with same keystore. but after every test we explicitly logging out.
tests := []struct {
name string
fn func(t *testing.T, feed *event.Feed) bool
}{
{
"StopResumeNode",
testStopResumeNode,
},
{
"RPCInProc",
testCallRPC,
},
{
"RPCPrivateAPI",
testCallRPCWithPrivateAPI,
},
{
"RPCPrivateClient",
testCallPrivateRPCWithPrivateAPI,
},
{
"VerifyAccountPassword",
testVerifyAccountPassword,
},
{
"RecoverAccount",
testRecoverAccount,
},
{
"LoginKeycard",
testLoginWithKeycard,
},
{
"AccountLoout",
testAccountLogout,
},
{
"SendTransaction",
testSendTransactionWithLogin,
},
{
"SendTransactionInvalidPassword",
testSendTransactionInvalidPassword,
},
{
"SendTransactionFailed",
testFailedTransaction,
},
{
"MultiAccount/Generate/Derive/StoreDerived/Load/Reset",
testMultiAccountGenerateDeriveStoreLoadReset,
},
{
"MultiAccount/ImportMnemonic/Derive",
testMultiAccountImportMnemonicAndDerive,
},
{
"MultiAccount/GenerateAndDerive",
testMultiAccountGenerateAndDerive,
},
{
"MultiAccount/Import/Store",
testMultiAccountImportStore,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
feed := &event.Feed{}
signal.SetDefaultNodeNotificationHandler(func(jsonEvent string) {
var envelope signal.Envelope
require.NoError(t, json.Unmarshal([]byte(jsonEvent), &envelope))
feed.Send(envelope)
})
defer func() {
if snode := statusBackend.StatusNode(); snode == nil || !snode.IsRunning() {
return
}
Logout()
waitSignal(feed, signal.EventNodeStopped, 5*time.Second)
}()
require.True(t, tc.fn(t, feed))
})
}
}
func testVerifyAccountPassword(t *testing.T, feed *event.Feed) bool {
tmpDir, err := ioutil.TempDir(os.TempDir(), "accounts")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpDir) // nolint: errcheck
if err = ImportTestAccount(tmpDir, GetAccount1PKFile()); err != nil {
t.Fatal(err)
}
if err = ImportTestAccount(tmpDir, GetAccount2PKFile()); err != nil {
t.Fatal(err)
}
// rename account file (to see that file's internals reviewed, when locating account key)
accountFilePathOriginal := filepath.Join(tmpDir, GetAccount1PKFile())
accountFilePath := filepath.Join(tmpDir, "foo"+TestConfig.Account1.WalletAddress+"bar.pk")
if err := os.Rename(accountFilePathOriginal, accountFilePath); err != nil {
t.Fatal(err)
}
response := APIResponse{}
rawResponse := VerifyAccountPassword(
C.CString(tmpDir),
C.CString(TestConfig.Account1.WalletAddress),
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 testStopResumeNode(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
account1 := createAccountAndLogin(t, feed)
whisperService, err := statusBackend.StatusNode().WhisperService()
require.NoError(t, err)
require.True(t, whisperService.HasKeyPair(account1.ChatPubKey), "whisper should have keypair")
response := APIResponse{}
rawResponse := StopNode()
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &response))
require.Empty(t, response.Error)
require.NoError(t, waitSignal(feed, signal.EventNodeStopped, 3*time.Second))
response = APIResponse{}
rawResponse = StartNode(C.CString(nodeConfigJSON))
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &response))
require.Empty(t, response.Error)
require.NoError(t, waitSignal(feed, signal.EventNodeReady, 5*time.Second))
// now, verify that we still have account logged in
whisperService, err = statusBackend.StatusNode().WhisperService()
require.NoError(t, err)
require.True(t, whisperService.HasKeyPair(account1.ChatPubKey))
return true
}
func testCallRPC(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
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 testCallRPCWithPrivateAPI(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
expected := `{"jsonrpc":"2.0","id":64,"error":{"code":-32601,"message":"the method admin_nodeInfo does not exist/is not available"}}`
rawResponse := CallRPC(C.CString(`{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":64}`))
require.Equal(t, expected, C.GoString(rawResponse))
return true
}
func testCallPrivateRPCWithPrivateAPI(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
rawResponse := CallPrivateRPC(C.CString(`{"jsonrpc":"2.0","method":"admin_nodeInfo","params":[],"id":64}`))
received := C.GoString(rawResponse)
if strings.Contains(received, "error") {
t.Errorf("unexpected response containing error: %v", received)
return false
}
return true
}
func testRecoverAccount(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
keyStore := statusBackend.AccountManager().GetKeystore()
require.NotNil(t, keyStore)
// create an account
accountInfo, mnemonic, err := statusBackend.AccountManager().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}", accountInfo.WalletAddress, accountInfo.WalletPubKey, 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
}
if recoverAccountResponse.Address != recoverAccountResponse.WalletAddress ||
recoverAccountResponse.PubKey != recoverAccountResponse.WalletPubKey {
t.Error("for backward compatibility pubkey/address should be equal to walletAddress/walletPubKey")
}
walletAddressCheck, walletPubKeyCheck := recoverAccountResponse.Address, recoverAccountResponse.PubKey
chatAddressCheck, chatPubKeyCheck := recoverAccountResponse.ChatAddress, recoverAccountResponse.ChatPubKey
if accountInfo.WalletAddress != walletAddressCheck || accountInfo.WalletPubKey != walletPubKeyCheck {
t.Error("recover wallet account details failed to pull the correct details")
}
if accountInfo.ChatAddress != chatAddressCheck || accountInfo.ChatPubKey != chatPubKeyCheck {
t.Error("recover chat 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(accountInfo.WalletAddress)
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
}
walletAddressCheck, walletPubKeyCheck = recoverAccountResponse.Address, recoverAccountResponse.PubKey
if accountInfo.WalletAddress != walletAddressCheck || accountInfo.WalletPubKey != walletPubKeyCheck {
t.Error("recover wallet account details failed to pull the correct details (for non-cached account)")
}
chatAddressCheck, chatPubKeyCheck = recoverAccountResponse.ChatAddress, recoverAccountResponse.ChatPubKey
if accountInfo.ChatAddress != chatAddressCheck || accountInfo.ChatPubKey != chatPubKeyCheck {
t.Error("recover chat 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
}
walletAddressCheck, walletPubKeyCheck = recoverAccountResponse.Address, recoverAccountResponse.PubKey
if accountInfo.WalletAddress != walletAddressCheck || accountInfo.WalletPubKey != walletPubKeyCheck {
t.Error("recover wallet account details failed to pull the correct details (for non-cached account)")
}
chatAddressCheck, chatPubKeyCheck = recoverAccountResponse.ChatAddress, recoverAccountResponse.ChatPubKey
if accountInfo.ChatAddress != chatAddressCheck || accountInfo.ChatPubKey != chatPubKeyCheck {
t.Error("recover chat account details failed to pull the correct details (for non-cached account)")
}
loginResponse := APIResponse{}
rawResponse = SaveAccountAndLogin(buildAccountData("test", walletAddressCheck), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(walletAddressCheck))
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
require.Empty(t, loginResponse.Error)
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
// time to login with recovered data
whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
if !whisperService.HasKeyPair(chatPubKeyCheck) {
t.Errorf("identity not injected into whisper: %v", err)
}
return true
}
func testLoginWithKeycard(t *testing.T, feed *event.Feed) bool { //nolint: gocyclo
createAccountAndLogin(t, feed)
chatPrivKey, err := crypto.GenerateKey()
if err != nil {
t.Errorf("error generating chat key")
return false
}
chatPrivKeyHex := hex.EncodeToString(crypto.FromECDSA(chatPrivKey))
encryptionPrivKey, err := crypto.GenerateKey()
if err != nil {
t.Errorf("error generating encryption key")
return false
}
encryptionPrivKeyHex := hex.EncodeToString(crypto.FromECDSA(encryptionPrivKey))
whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
}
chatPubKeyHex := types.EncodeHex(crypto.FromECDSAPub(&chatPrivKey.PublicKey))
if whisperService.HasKeyPair(chatPubKeyHex) {
t.Error("identity already present in whisper")
return false
}
loginResponse := APIResponse{}
rawResponse := LoginWithKeycard(C.CString(chatPrivKeyHex), C.CString(encryptionPrivKeyHex))
if err = json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse); err != nil {
t.Errorf("cannot decode LoginWithKeycard response (%s): %v", C.GoString(rawResponse), err)
return false
}
if loginResponse.Error != "" {
t.Errorf("Test failed: could not login with keycard: %v", err)
return false
}
if !whisperService.HasKeyPair(chatPubKeyHex) {
t.Error("identity not present in whisper after logging in with keycard")
return false
}
return true
}
func testAccountLogout(t *testing.T, feed *event.Feed) bool {
accountInfo := createAccountAndLogin(t, feed)
whisperService, err := statusBackend.StatusNode().WhisperService()
if err != nil {
t.Errorf("whisper service not running: %v", err)
return false
}
if !whisperService.HasKeyPair(accountInfo.ChatPubKey) {
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(accountInfo.ChatPubKey) {
t.Error("identity not cleared from whisper")
return false
}
return true
}
type jsonrpcAnyResponse struct {
Result json.RawMessage `json:"result"`
jsonrpcErrorResponse
}
func testSendTransactionWithLogin(t *testing.T, feed *event.Feed) bool {
loginResponse := APIResponse{}
rawResponse := SaveAccountAndLogin(buildAccountData("test", TestConfig.Account1.WalletAddress), C.CString(TestConfig.Account1.Password), C.CString(nodeConfigJSON), buildSubAccountData(TestConfig.Account1.WalletAddress))
require.NoError(t, json.Unmarshal([]byte(C.GoString(rawResponse)), &loginResponse))
require.Empty(t, loginResponse.Error)
require.NoError(t, waitSignal(feed, signal.EventLoggedIn, 5*time.Second))
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
args, err := json.Marshal(transactions.SendTxArgs{
From: account.FromAddress(TestConfig.Account1.WalletAddress),
To: account.ToAddress(TestConfig.Account2.WalletAddress),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
t.Errorf("failed to marshal errors: %v", err)
return false
}
rawResult := SendTransaction(C.CString(string(args)), C.CString(TestConfig.Account1.Password))
var result jsonrpcAnyResponse
if err := json.Unmarshal([]byte(C.GoString(rawResult)), &result); err != nil {
t.Errorf("failed to unmarshal rawResult '%s': %v", C.GoString(rawResult), err)
return false
}
if result.Error.Message != "" {
t.Errorf("failed to send transaction: %v", result.Error)
return false
}
hash := common.BytesToHash(result.Result)
if reflect.DeepEqual(hash, common.Hash{}) {
t.Errorf("response hash empty: %s", hash.Hex())
return false
}
return true
}
func testSendTransactionInvalidPassword(t *testing.T, feed *event.Feed) bool {
acc := createAccountAndLogin(t, feed)
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
args, err := json.Marshal(transactions.SendTxArgs{
From: common.HexToAddress(acc.WalletAddress),
To: account.ToAddress(TestConfig.Account2.WalletAddress),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
t.Errorf("failed to marshal errors: %v", err)
return false
}
rawResult := SendTransaction(C.CString(string(args)), C.CString("invalid password"))
var result jsonrpcAnyResponse
if err := json.Unmarshal([]byte(C.GoString(rawResult)), &result); err != nil {
t.Errorf("failed to unmarshal rawResult '%s': %v", C.GoString(rawResult), err)
return false
}
if result.Error.Message != keystore.ErrDecrypt.Error() {
t.Errorf("invalid result: %q", result)
return false
}
return true
}
func testFailedTransaction(t *testing.T, feed *event.Feed) bool {
createAccountAndLogin(t, feed)
EnsureNodeSync(statusBackend.StatusNode().EnsureSync)
args, err := json.Marshal(transactions.SendTxArgs{
From: *account.ToAddress(TestConfig.Account1.WalletAddress),
To: account.ToAddress(TestConfig.Account2.WalletAddress),
Value: (*hexutil.Big)(big.NewInt(1000000000000)),
})
if err != nil {
t.Errorf("failed to marshal errors: %v", err)
return false
}
rawResult := SendTransaction(C.CString(string(args)), C.CString(TestConfig.Account1.Password))
var result jsonrpcAnyResponse
if err := json.Unmarshal([]byte(C.GoString(rawResult)), &result); err != nil {
t.Errorf("failed to unmarshal rawResult '%s': %v", C.GoString(rawResult), err)
return false
}
if result.Error.Message != transactions.ErrAccountDoesntExist.Error() {
t.Errorf("expected error to be ErrAccountDoesntExist, got %s", result.Error.Message)
return false
}
if result.Result != nil {
t.Errorf("expected result to be nil")
return false
}
return true
}
//nolint: deadcode
func testValidateNodeConfig(t *testing.T, config string, fn func(*testing.T, APIDetailedResponse)) {
result := ValidateNodeConfig(C.CString(config))
var resp APIDetailedResponse
err := json.Unmarshal([]byte(C.GoString(result)), &resp)
require.NoError(t, err)
fn(t, resp)
}