status-go/api/backend_test.go

596 lines
16 KiB
Go

package api
import (
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"sync"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
"github.com/status-im/status-go/node"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/typeddata"
"github.com/status-im/status-go/t/utils"
"github.com/status-im/status-go/transactions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestBackendStartNodeConcurrently(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
count := 2
resultCh := make(chan error)
var wg sync.WaitGroup
wg.Add(count)
for i := 0; i < count; i++ {
go func() {
resultCh <- backend.StartNode(config)
wg.Done()
}()
}
// close channel as otherwise for loop never finishes
go func() { wg.Wait(); close(resultCh) }()
var results []error
for err := range resultCh {
results = append(results, err)
}
require.Contains(t, results, nil)
require.Contains(t, results, node.ErrNodeRunning)
err = backend.StopNode()
require.NoError(t, err)
}
func TestBackendRestartNodeConcurrently(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
count := 3
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
require.NoError(t, backend.StartNode(config))
defer func() {
require.NoError(t, backend.StopNode())
}()
var wg sync.WaitGroup
wg.Add(count)
for i := 0; i < count; i++ {
go func(idx int) {
assert.NoError(t, backend.RestartNode())
wg.Done()
}(i)
}
wg.Wait()
}
// TODO(adam): add concurrent tests for ResetChainData()
func TestBackendGettersConcurrently(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {
require.NoError(t, backend.StopNode())
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
assert.NotNil(t, backend.StatusNode())
wg.Done()
}()
wg.Add(1)
go func() {
assert.NotNil(t, backend.AccountManager())
wg.Done()
}()
wg.Add(1)
go func() {
assert.NotNil(t, backend.personalAPI)
wg.Done()
}()
wg.Add(1)
go func() {
assert.NotNil(t, backend.Transactor())
wg.Done()
}()
wg.Add(1)
go func() {
assert.True(t, backend.IsNodeRunning())
wg.Done()
}()
wg.Add(1)
go func() {
assert.True(t, backend.IsNodeRunning())
wg.Done()
}()
wg.Wait()
}
func TestBackendAccountsConcurrently(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {
require.NoError(t, backend.StopNode())
}()
var wgCreateAccounts sync.WaitGroup
count := 3
addressCh := make(chan [3]string, count) // use buffered channel to avoid blocking
// create new accounts concurrently
for i := 0; i < count; i++ {
wgCreateAccounts.Add(1)
go func(pass string) {
accountInfo, _, err := backend.AccountManager().CreateAccount(pass)
assert.NoError(t, err)
addressCh <- [...]string{accountInfo.WalletAddress, accountInfo.ChatAddress, pass}
wgCreateAccounts.Done()
}("password-00" + string(i))
}
// close addressCh as otherwise for loop never finishes
go func() { wgCreateAccounts.Wait(); close(addressCh) }()
// select, reselect or logout concurrently
var wg sync.WaitGroup
for tuple := range addressCh {
wg.Add(1)
go func(tuple [3]string) {
loginParams := account.LoginParams{
MainAccount: common.HexToAddress(tuple[0]),
ChatAddress: common.HexToAddress(tuple[1]),
Password: tuple[2],
}
assert.NoError(t, backend.SelectAccount(loginParams))
wg.Done()
}(tuple)
wg.Add(1)
go func() {
assert.NoError(t, backend.reSelectAccount())
wg.Done()
}()
wg.Add(1)
go func() {
assert.NoError(t, backend.Logout())
wg.Done()
}()
}
wg.Wait()
}
func TestBackendInjectChatAccount(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {
require.NoError(t, backend.StopNode())
}()
chatPrivKey, err := crypto.GenerateKey()
require.NoError(t, err)
encryptionPrivKey, err := crypto.GenerateKey()
require.NoError(t, err)
chatPrivKeyHex := hex.EncodeToString(crypto.FromECDSA(chatPrivKey))
chatPubKeyHex := hexutil.Encode(crypto.FromECDSAPub(&chatPrivKey.PublicKey))
encryptionPrivKeyHex := hex.EncodeToString(crypto.FromECDSA(encryptionPrivKey))
whisperService, err := backend.StatusNode().WhisperService()
require.NoError(t, err)
// public key should not be already in whisper
require.False(t, whisperService.HasKeyPair(chatPubKeyHex), "identity already present in whisper")
// call InjectChatAccount
require.NoError(t, backend.InjectChatAccount(chatPrivKeyHex, encryptionPrivKeyHex))
// public key should now be in whisper
require.True(t, whisperService.HasKeyPair(chatPubKeyHex), "identity not injected into whisper")
// wallet account should not be selected
mainAccountAddress, err := backend.AccountManager().MainAccountAddress()
require.Equal(t, common.Address{}, mainAccountAddress)
require.Equal(t, account.ErrNoAccountSelected, err)
// selected chat account should have the key injected previously
chatAcc, err := backend.AccountManager().SelectedChatAccount()
require.Nil(t, err)
require.Equal(t, chatPrivKey, chatAcc.AccountKey.PrivateKey)
}
func TestBackendConnectionChangesConcurrently(t *testing.T) {
connections := [...]string{wifi, cellular, unknown}
backend := NewStatusBackend()
count := 3
var wg sync.WaitGroup
for i := 0; i < count; i++ {
wg.Add(1)
go func() {
connIdx := rand.Intn(len(connections))
backend.ConnectionChange(connections[connIdx], false)
wg.Done()
}()
}
wg.Wait()
}
func TestBackendConnectionChangesToOffline(t *testing.T) {
b := NewStatusBackend()
b.ConnectionChange(none, false)
assert.True(t, b.connectionState.Offline)
b.ConnectionChange(wifi, false)
assert.False(t, b.connectionState.Offline)
b.ConnectionChange("unknown-state", false)
assert.False(t, b.connectionState.Offline)
}
func TestBackendCallRPCConcurrently(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
count := 3
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {
require.NoError(t, backend.StopNode())
}()
var wg sync.WaitGroup
for i := 0; i < count; i++ {
wg.Add(1)
go func(idx int) {
result, err := backend.CallRPC(fmt.Sprintf(
`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":%d}`,
idx+1,
))
assert.NoError(t, err)
assert.NotContains(t, result, "error")
wg.Done()
}(i)
wg.Add(1)
go func(idx int) {
result, err := backend.CallPrivateRPC(fmt.Sprintf(
`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":%d}`,
idx+1,
))
assert.NoError(t, err)
assert.NotContains(t, result, "error")
wg.Done()
}(i)
}
wg.Wait()
}
func TestAppStateChange(t *testing.T) {
backend := NewStatusBackend()
var testCases = []struct {
name string
fromState appState
toState appState
expectedState appState
}{
{
name: "success",
fromState: appStateInactive,
toState: appStateBackground,
expectedState: appStateBackground,
},
{
name: "invalid state",
fromState: appStateInactive,
toState: "unexisting",
expectedState: appStateInactive,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
backend.appState = tc.fromState
backend.AppStateChange(tc.toState.String())
assert.Equal(t, tc.expectedState.String(), backend.appState.String())
})
}
}
func TestBlockedRPCMethods(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() { require.NoError(t, backend.StopNode()) }()
for idx, m := range rpc.BlockedMethods() {
result, err := backend.CallRPC(fmt.Sprintf(
`{"jsonrpc":"2.0","method":"%s","params":[],"id":%d}`,
m,
idx+1,
))
assert.NoError(t, err)
assert.Contains(t, result, fmt.Sprintf(`{"code":-32700,"message":"%s"}`, rpc.ErrMethodNotFound))
}
}
func TestCallRPCWithStoppedNode(t *testing.T) {
backend := NewStatusBackend()
resp, err := backend.CallRPC(
`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}`,
)
assert.Equal(t, ErrRPCClientUnavailable, err)
assert.Equal(t, "", resp)
resp, err = backend.CallPrivateRPC(
`{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}`,
)
assert.Equal(t, ErrRPCClientUnavailable, err)
assert.Equal(t, "", resp)
}
// TODO(adam): add concurrent tests for: SendTransaction
func TestStartStopMultipleTimes(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
config.NoDiscovery = false
// doesn't have to be running. just any valid enode to bypass validation.
config.ClusterConfig.BootNodes = []string{
"enode://e8a7c03b58911e98bbd66accb2a55d57683f35b23bf9dfca89e5e244eb5cc3f25018b4112db507faca34fb69ffb44b362f79eda97a669a8df29c72e654416784@0.0.0.0:30404",
}
require.NoError(t, err)
require.NoError(t, backend.StartNode(config))
require.NoError(t, backend.StopNode())
require.NoError(t, backend.StartNode(config))
require.NoError(t, backend.StopNode())
}
func TestSignHash(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
require.NoError(t, backend.StartNode(config))
defer func() {
require.NoError(t, backend.StopNode())
}()
var testCases = []struct {
name string
chatPrivKeyHex string
hashHex string
expectedSignatureHex string
}{
{
name: "tc1",
chatPrivKeyHex: "facadefacadefacadefacadefacadefacadefacadefacadefacadefacadefaca",
hashHex: "0xa4735de5193362fe856416000105cdfa6ce56265607311cebae93b26e5adf438",
expectedSignatureHex: "0x176c971bae188c663614fc535ac9dbf62871dfeaadb38645809a510d28b3c4b0245415d5547c1b27f7cfea3341564f9c6981421144d3606b455346be69bd078c01",
},
}
const dummyEncKey = "facadefacadefacadefacadefacadefacadefacadefacadefacadefacadefaca"
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
require.NoError(t, backend.InjectChatAccount(tc.chatPrivKeyHex, dummyEncKey))
signature, err := backend.SignHash(tc.hashHex)
require.NoError(t, err)
require.Equal(t, signature, tc.expectedSignatureHex)
})
}
}
func TestHashTypedData(t *testing.T) {
utils.Init()
backend := NewStatusBackend()
config, err := utils.MakeTestNodeConfig(params.StatusChainNetworkID)
require.NoError(t, err)
require.NoError(t, backend.AccountManager().InitKeystore(config.KeyStoreDir))
err = backend.StartNode(config)
require.NoError(t, err)
defer func() {
require.NoError(t, backend.StopNode())
}()
eip712Domain := "EIP712Domain"
mytypes := typeddata.Types{
eip712Domain: []typeddata.Field{
{Name: "name", Type: "string"},
{Name: "version", Type: "string"},
{Name: "chainId", Type: "uint256"},
{Name: "verifyingContract", Type: "address"},
},
"Text": []typeddata.Field{
{Name: "body", Type: "string"},
},
}
domain := map[string]json.RawMessage{
"name": json.RawMessage(`"Ether Text"`),
"version": json.RawMessage(`"1"`),
"chainId": json.RawMessage(fmt.Sprintf("%d", params.StatusChainNetworkID)),
"verifyingContract": json.RawMessage(`"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"`),
}
msg := map[string]json.RawMessage{
"body": json.RawMessage(`"Hello, Bob!"`),
}
typed := typeddata.TypedData{
Types: mytypes,
PrimaryType: "Text",
Domain: domain,
Message: msg,
}
hash, err := backend.HashTypedData(typed)
require.NoError(t, err)
assert.NotEqual(t, common.Hash{}, hash)
}
func TestBackendGetVerifiedAccount(t *testing.T) {
utils.Init()
password := "test"
tmpdir, err := ioutil.TempDir("", "verified-account-test-")
require.NoError(t, err)
defer os.Remove(tmpdir)
backend := NewStatusBackend()
backend.UpdateRootDataDir(tmpdir)
require.NoError(t, backend.accountManager.InitKeystore(filepath.Join(tmpdir, "keystore")))
require.NoError(t, backend.ensureAppDBOpened(multiaccounts.Account{Address: common.Address{1, 1, 1}}, password))
config, err := params.NewNodeConfig(tmpdir, 178733)
require.NoError(t, err)
// this is for StatusNode().Config() call inside of the getVerifiedWalletAccount
require.NoError(t, backend.StartNode(config))
defer func() {
require.NoError(t, backend.StopNode())
}()
t.Run("AccountDoesntExist", func(t *testing.T) {
pkey, err := crypto.GenerateKey()
require.NoError(t, err)
address := crypto.PubkeyToAddress(pkey.PublicKey)
key, err := backend.getVerifiedWalletAccount(address.String(), password)
require.EqualError(t, err, transactions.ErrAccountDoesntExist.Error())
require.Nil(t, key)
})
t.Run("PasswordDoesntMatch", func(t *testing.T) {
pkey, err := crypto.GenerateKey()
require.NoError(t, err)
address := crypto.PubkeyToAddress(pkey.PublicKey)
db := accounts.NewDB(backend.appDB)
_, err = backend.accountManager.ImportAccount(pkey, password)
require.NoError(t, err)
require.NoError(t, db.SaveAccounts([]accounts.Account{{Address: address}}))
key, err := backend.getVerifiedWalletAccount(address.String(), "wrong-password")
require.EqualError(t, err, "could not decrypt key with given password")
require.Nil(t, key)
})
t.Run("Success", func(t *testing.T) {
pkey, err := crypto.GenerateKey()
require.NoError(t, err)
address := crypto.PubkeyToAddress(pkey.PublicKey)
db := accounts.NewDB(backend.appDB)
_, err = backend.accountManager.ImportAccount(pkey, password)
require.NoError(t, err)
require.NoError(t, db.SaveAccounts([]accounts.Account{{Address: address}}))
key, err := backend.getVerifiedWalletAccount(address.String(), password)
require.NoError(t, err)
require.Equal(t, address, key.Address)
})
}
func TestLoginWithKey(t *testing.T) {
utils.Init()
b := NewStatusBackend()
pkey, err := crypto.GenerateKey()
require.NoError(t, err)
main := multiaccounts.Account{
Address: crypto.PubkeyToAddress(pkey.PublicKey),
}
tmpdir, err := ioutil.TempDir("", "login-with-key-test-")
require.NoError(t, err)
defer os.Remove(tmpdir)
conf, err := params.NewNodeConfig(tmpdir, 1777)
require.NoError(t, err)
keyhex := hex.EncodeToString(crypto.FromECDSA(pkey))
require.NoError(t, b.accountManager.InitKeystore(conf.KeyStoreDir))
b.UpdateRootDataDir(conf.DataDir)
require.NoError(t, b.OpenAccounts())
require.NoError(t, b.SaveAccountAndStartNodeWithKey(main, conf, "test-pass", keyhex))
require.NoError(t, b.Logout())
require.NoError(t, b.StopNode())
require.NoError(t, b.StartNodeWithKey(main, "test-pass", keyhex))
defer func() {
assert.NoError(t, b.Logout())
assert.NoError(t, b.StopNode())
}()
extkey, err := b.accountManager.SelectedChatAccount()
require.NoError(t, err)
require.Equal(t, crypto.PubkeyToAddress(pkey.PublicKey), extkey.Address)
}