Expose status specific methods (login/signup/joinpublicchannel) through the RPC api (#877)

This commit is contained in:
Adrià Cidre 2018-05-03 12:36:56 +02:00 committed by GitHub
parent 953c26e8cf
commit 8c9db81bec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 806 additions and 61 deletions

View File

@ -116,6 +116,7 @@ mock: ##@other Regenerate mocks
mockgen -package=fake -destination=geth/transactions/fake/mock.go -source=geth/transactions/fake/txservice.go mockgen -package=fake -destination=geth/transactions/fake/mock.go -source=geth/transactions/fake/txservice.go
mockgen -package=account -destination=geth/account/accounts_mock.go -source=geth/account/accounts.go mockgen -package=account -destination=geth/account/accounts_mock.go -source=geth/account/accounts.go
mockgen -package=jail -destination=geth/jail/cell_mock.go -source=geth/jail/cell.go mockgen -package=jail -destination=geth/jail/cell_mock.go -source=geth/jail/cell.go
mockgen -package=status -destination=services/status/account_mock.go -source=services/status/service.go
docker-test: ##@tests Run tests in a docker container with golang. docker-test: ##@tests Run tests in a docker container with golang.
docker run --privileged --rm -it -v "$(shell pwd):$(DOCKER_TEST_WORKDIR)" -w "$(DOCKER_TEST_WORKDIR)" $(DOCKER_TEST_IMAGE) go test ${ARGS} docker run --privileged --rm -it -v "$(shell pwd):$(DOCKER_TEST_WORKDIR)" -w "$(DOCKER_TEST_WORKDIR)" $(DOCKER_TEST_IMAGE) go test ${ARGS}

View File

@ -150,6 +150,10 @@ func (b *StatusBackend) startNode(config *params.NodeConfig) (err error) {
} }
b.log.Info("Account reselected") b.log.Info("Account reselected")
if st, err := b.statusNode.StatusService(); err == nil {
st.SetAccountManager(b.AccountManager())
}
signal.SendNodeReady() signal.SendNodeReady()
return nil return nil

View File

@ -25,6 +25,7 @@ import (
shhmetrics "github.com/status-im/status-go/metrics/whisper" shhmetrics "github.com/status-im/status-go/metrics/whisper"
"github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/services/shhext" "github.com/status-im/status-go/services/shhext"
"github.com/status-im/status-go/services/status"
) )
// Errors related to node and services creation. // Errors related to node and services creation.
@ -33,6 +34,7 @@ var (
ErrWhisperServiceRegistrationFailure = errors.New("failed to register the Whisper service") ErrWhisperServiceRegistrationFailure = errors.New("failed to register the Whisper service")
ErrLightEthRegistrationFailure = errors.New("failed to register the LES service") ErrLightEthRegistrationFailure = errors.New("failed to register the LES service")
ErrPersonalServiceRegistrationFailure = errors.New("failed to register the personal api service") ErrPersonalServiceRegistrationFailure = errors.New("failed to register the personal api service")
ErrStatusServiceRegistrationFailure = errors.New("failed to register the Status service")
) )
// All general log messages in this package should be routed through this logger. // All general log messages in this package should be routed through this logger.
@ -93,6 +95,11 @@ func MakeNode(config *params.NodeConfig) (*node.Node, error) {
return nil, fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err) return nil, fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)
} }
// start status service.
if err := activateStatusService(stack, config); err != nil {
return nil, fmt.Errorf("%v: %v", ErrStatusServiceRegistrationFailure, err)
}
return stack, nil return stack, nil
} }
@ -163,6 +170,22 @@ func activatePersonalService(stack *node.Node, config *params.NodeConfig) error
}) })
} }
func activateStatusService(stack *node.Node, config *params.NodeConfig) error {
if !config.StatusServiceEnabled {
logger.Info("Status service api is disabled")
return nil
}
return stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
var whisper *whisper.Whisper
if err := ctx.Service(&whisper); err != nil {
return nil, err
}
svc := status.New(whisper)
return svc, nil
})
}
// activateShhService configures Whisper and adds it to the given node. // activateShhService configures Whisper and adds it to the given node.
func activateShhService(stack *node.Node, config *params.NodeConfig) (err error) { func activateShhService(stack *node.Node, config *params.NodeConfig) (err error) {
if config.WhisperConfig == nil || !config.WhisperConfig.Enabled { if config.WhisperConfig == nil || !config.WhisperConfig.Enabled {

View File

@ -23,6 +23,7 @@ import (
"github.com/status-im/status-go/geth/peers" "github.com/status-im/status-go/geth/peers"
"github.com/status-im/status-go/geth/rpc" "github.com/status-im/status-go/geth/rpc"
"github.com/status-im/status-go/services/shhext" "github.com/status-im/status-go/services/shhext"
"github.com/status-im/status-go/services/status"
) )
// tickerResolution is the delta to check blockchain sync progress. // tickerResolution is the delta to check blockchain sync progress.
@ -393,6 +394,19 @@ func (n *StatusNode) LightEthereumService() (l *les.LightEthereum, err error) {
return return
} }
// StatusService exposes reference to status service running on top of the node
func (n *StatusNode) StatusService() (st *status.Service, err error) {
n.mu.RLock()
defer n.mu.RUnlock()
err = n.gethService(&st)
if err == node.ErrServiceUnknown {
err = ErrServiceUnknown
}
return
}
// WhisperService exposes reference to Whisper service running on top of the node // WhisperService exposes reference to Whisper service running on top of the node
func (n *StatusNode) WhisperService() (w *whisper.Whisper, err error) { func (n *StatusNode) WhisperService() (w *whisper.Whisper, err error) {
n.mu.RLock() n.mu.RLock()

View File

@ -306,6 +306,9 @@ type NodeConfig struct {
RegisterTopics []discv5.Topic `json:"RegisterTopics"` RegisterTopics []discv5.Topic `json:"RegisterTopics"`
RequireTopics map[discv5.Topic]Limits `json:"RequireTopics"` RequireTopics map[discv5.Topic]Limits `json:"RequireTopics"`
// StatusServiceEnabled enables status service api
StatusServiceEnabled bool
} }
// NewNodeConfig creates new node configuration object // NewNodeConfig creates new node configuration object

View File

@ -0,0 +1,150 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: services/status/service.go
// Package status is a generated GoMock package.
package status
import (
ecdsa "crypto/ecdsa"
accounts "github.com/ethereum/go-ethereum/accounts"
keystore "github.com/ethereum/go-ethereum/accounts/keystore"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
)
// MockWhisper is a mock of Whisper interface
type MockWhisper struct {
ctrl *gomock.Controller
recorder *MockWhisperMockRecorder
}
// MockWhisperMockRecorder is the mock recorder for MockWhisper
type MockWhisperMockRecorder struct {
mock *MockWhisper
}
// NewMockWhisper creates a new mock instance
func NewMockWhisper(ctrl *gomock.Controller) *MockWhisper {
mock := &MockWhisper{ctrl: ctrl}
mock.recorder = &MockWhisperMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockWhisper) EXPECT() *MockWhisperMockRecorder {
return m.recorder
}
// AddKeyPair mocks base method
func (m *MockWhisper) AddKeyPair(key *ecdsa.PrivateKey) (string, error) {
ret := m.ctrl.Call(m, "AddKeyPair", key)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AddKeyPair indicates an expected call of AddKeyPair
func (mr *MockWhisperMockRecorder) AddKeyPair(key interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddKeyPair", reflect.TypeOf((*MockWhisper)(nil).AddKeyPair), key)
}
// MockAccountManager is a mock of AccountManager interface
type MockAccountManager struct {
ctrl *gomock.Controller
recorder *MockAccountManagerMockRecorder
}
// MockAccountManagerMockRecorder is the mock recorder for MockAccountManager
type MockAccountManagerMockRecorder struct {
mock *MockAccountManager
}
// NewMockAccountManager creates a new mock instance
func NewMockAccountManager(ctrl *gomock.Controller) *MockAccountManager {
mock := &MockAccountManager{ctrl: ctrl}
mock.recorder = &MockAccountManagerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockAccountManager) EXPECT() *MockAccountManagerMockRecorder {
return m.recorder
}
// AddressToDecryptedAccount mocks base method
func (m *MockAccountManager) AddressToDecryptedAccount(arg0, arg1 string) (accounts.Account, *keystore.Key, error) {
ret := m.ctrl.Call(m, "AddressToDecryptedAccount", arg0, arg1)
ret0, _ := ret[0].(accounts.Account)
ret1, _ := ret[1].(*keystore.Key)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// AddressToDecryptedAccount indicates an expected call of AddressToDecryptedAccount
func (mr *MockAccountManagerMockRecorder) AddressToDecryptedAccount(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddressToDecryptedAccount", reflect.TypeOf((*MockAccountManager)(nil).AddressToDecryptedAccount), arg0, arg1)
}
// SelectAccount mocks base method
func (m *MockAccountManager) SelectAccount(address, password string) error {
ret := m.ctrl.Call(m, "SelectAccount", address, password)
ret0, _ := ret[0].(error)
return ret0
}
// SelectAccount indicates an expected call of SelectAccount
func (mr *MockAccountManagerMockRecorder) SelectAccount(address, password interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectAccount", reflect.TypeOf((*MockAccountManager)(nil).SelectAccount), address, password)
}
// CreateAccount mocks base method
func (m *MockAccountManager) CreateAccount(password string) (string, string, string, error) {
ret := m.ctrl.Call(m, "CreateAccount", password)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(string)
ret2, _ := ret[2].(string)
ret3, _ := ret[3].(error)
return ret0, ret1, ret2, ret3
}
// CreateAccount indicates an expected call of CreateAccount
func (mr *MockAccountManagerMockRecorder) CreateAccount(password interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccount", reflect.TypeOf((*MockAccountManager)(nil).CreateAccount), password)
}
// MockWhisperService is a mock of WhisperService interface
type MockWhisperService struct {
ctrl *gomock.Controller
recorder *MockWhisperServiceMockRecorder
}
// MockWhisperServiceMockRecorder is the mock recorder for MockWhisperService
type MockWhisperServiceMockRecorder struct {
mock *MockWhisperService
}
// NewMockWhisperService creates a new mock instance
func NewMockWhisperService(ctrl *gomock.Controller) *MockWhisperService {
mock := &MockWhisperService{ctrl: ctrl}
mock.recorder = &MockWhisperServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockWhisperService) EXPECT() *MockWhisperServiceMockRecorder {
return m.recorder
}
// AddressToDecryptedAccount mocks base method
func (m *MockWhisperService) AddressToDecryptedAccount(arg0, arg1 string) (accounts.Account, *keystore.Key, error) {
ret := m.ctrl.Call(m, "AddressToDecryptedAccount", arg0, arg1)
ret0, _ := ret[0].(accounts.Account)
ret1, _ := ret[1].(*keystore.Key)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// AddressToDecryptedAccount indicates an expected call of AddressToDecryptedAccount
func (mr *MockWhisperServiceMockRecorder) AddressToDecryptedAccount(arg0, arg1 interface{}) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddressToDecryptedAccount", reflect.TypeOf((*MockWhisperService)(nil).AddressToDecryptedAccount), arg0, arg1)
}

67
services/status/api.go Normal file
View File

@ -0,0 +1,67 @@
package status
import (
"context"
"errors"
)
// PublicAPI represents a set of APIs from the `web3.status` namespace.
type PublicAPI struct {
s *Service
}
// NewAPI creates an instance of the status API.
func NewAPI(s *Service) *PublicAPI {
return &PublicAPI{s: s}
}
// LoginRequest : json request for status_login.
type LoginRequest struct {
Addr string `json:"address"`
Password string `json:"password"`
}
// LoginResponse : json response returned by status_login.
type LoginResponse struct {
AddressKeyID string `json:"address_key_id"`
}
// Login is an implementation of `status_login` or `web3.status.login` API
func (api *PublicAPI) Login(context context.Context, req LoginRequest) (res LoginResponse, err error) {
_, accountKey, err := api.s.am.AddressToDecryptedAccount(req.Addr, req.Password)
if err != nil {
return
}
if res.AddressKeyID, err = api.s.w.AddKeyPair(accountKey.PrivateKey); err != nil {
return
}
if err = api.s.am.SelectAccount(req.Addr, req.Password); err != nil {
return
}
return
}
// SignupRequest : json request for status_signup.
type SignupRequest struct {
Password string `json:"password"`
}
// SignupResponse : json response returned by status_signup.
type SignupResponse struct {
Address string `json:"address"`
Pubkey string `json:"pubkey"`
Mnemonic string `json:"mnemonic"`
}
// Signup is an implementation of `status_signup` or `web3.status.signup` API
func (api *PublicAPI) Signup(context context.Context, req SignupRequest) (res SignupResponse, err error) {
if res.Address, res.Pubkey, res.Mnemonic, err = api.s.am.CreateAccount(req.Password); err != nil {
err = errors.New("could not create the specified account : " + err.Error())
return
}
return
}

149
services/status/api_test.go Normal file
View File

@ -0,0 +1,149 @@
package status
import (
"context"
"crypto/ecdsa"
"errors"
"testing"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/suite"
)
func TestStatusSuite(t *testing.T) {
suite.Run(t, new(StatusSuite))
}
type StatusSuite struct {
suite.Suite
am *MockAccountManager
w *MockWhisper
api *PublicAPI
}
func (s *StatusSuite) SetupTest() {
ctrl := gomock.NewController(s.T())
s.am = NewMockAccountManager(ctrl)
s.w = NewMockWhisper(ctrl)
service := New(s.w)
service.SetAccountManager(s.am)
s.api = NewAPI(service)
}
var logintests = []struct {
name string
expectedAddressKey string
expectedError error
prepareExpectations func(*StatusSuite)
}{
{
name: "success login",
expectedAddressKey: "addressKey",
expectedError: nil,
prepareExpectations: func(s *StatusSuite) {
key := keystore.Key{
PrivateKey: &ecdsa.PrivateKey{},
}
s.am.EXPECT().AddressToDecryptedAccount("address...", "password").Return(accounts.Account{}, &key, nil)
s.w.EXPECT().AddKeyPair(key.PrivateKey).Return("addressKey", nil)
s.am.EXPECT().SelectAccount("address...", "password").Return(nil)
},
},
{
name: "error when decrypting account from address",
expectedAddressKey: "",
expectedError: errors.New("foo"),
prepareExpectations: func(s *StatusSuite) {
key := keystore.Key{
PrivateKey: &ecdsa.PrivateKey{},
}
s.am.EXPECT().AddressToDecryptedAccount("address...", "password").Return(accounts.Account{}, &key, errors.New("foo"))
},
},
{
name: "error when adding key pair to whisper",
expectedAddressKey: "",
expectedError: errors.New("foo"),
prepareExpectations: func(s *StatusSuite) {
key := keystore.Key{
PrivateKey: &ecdsa.PrivateKey{},
}
s.am.EXPECT().AddressToDecryptedAccount("address...", "password").Return(accounts.Account{}, &key, nil)
s.w.EXPECT().AddKeyPair(key.PrivateKey).Return("", errors.New("foo"))
},
},
{
name: "error when selecting account",
expectedAddressKey: "",
expectedError: errors.New("foo"),
prepareExpectations: func(s *StatusSuite) {
key := keystore.Key{
PrivateKey: &ecdsa.PrivateKey{},
}
s.am.EXPECT().AddressToDecryptedAccount("address...", "password").Return(accounts.Account{}, &key, nil)
s.w.EXPECT().AddKeyPair(key.PrivateKey).Return("", nil)
s.am.EXPECT().SelectAccount("address...", "password").Return(errors.New("foo"))
},
},
}
func (s *StatusSuite) TestLogin() {
for _, t := range logintests {
req := LoginRequest{Addr: "address...", Password: "password"}
t.prepareExpectations(s)
var ctx context.Context
res, err := s.api.Login(ctx, req)
s.Equal(t.expectedAddressKey, res.AddressKeyID, "failed scenario : "+t.name)
s.Equal(t.expectedError, err, "failed scenario : "+t.name)
}
}
var signuptests = []struct {
name string
expectedResponse SignupResponse
expectedError error
prepareExpectations func(*StatusSuite)
}{
{
name: "success signup",
expectedResponse: SignupResponse{
Address: "addr",
Pubkey: "pubkey",
Mnemonic: "mnemonic",
},
expectedError: nil,
prepareExpectations: func(s *StatusSuite) {
s.am.EXPECT().CreateAccount("password").Return("addr", "pubkey", "mnemonic", nil)
},
},
{
name: "success signup",
expectedResponse: SignupResponse{
Address: "",
Pubkey: "",
Mnemonic: "",
},
expectedError: errors.New("could not create the specified account : foo"),
prepareExpectations: func(s *StatusSuite) {
s.am.EXPECT().CreateAccount("password").Return("", "", "", errors.New("foo"))
},
},
}
func (s *StatusSuite) TestSignup() {
for _, t := range signuptests {
t.prepareExpectations(s)
var ctx context.Context
res, err := s.api.Signup(ctx, SignupRequest{Password: "password"})
s.Equal(t.expectedResponse.Address, res.Address, "failed scenario : "+t.name)
s.Equal(t.expectedResponse.Pubkey, res.Pubkey, "failed scenario : "+t.name)
s.Equal(t.expectedResponse.Mnemonic, res.Mnemonic, "failed scenario : "+t.name)
s.Equal(t.expectedError, err, "failed scenario : "+t.name)
}
}

View File

@ -0,0 +1,72 @@
package status
import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/rpc"
)
// Make sure that Service implements node.Service interface.
var _ node.Service = (*Service)(nil)
// WhisperService whisper interface to add key pairs
type WhisperService interface {
AddKeyPair(key *ecdsa.PrivateKey) (string, error)
}
// AccountManager interface to manage account actions
type AccountManager interface {
AddressToDecryptedAccount(string, string) (accounts.Account, *keystore.Key, error)
SelectAccount(address, password string) error
CreateAccount(password string) (address, pubKey, mnemonic string, err error)
}
// Service represents out own implementation of status status operations.
type Service struct {
am AccountManager
w WhisperService
}
// New returns a new Service.
func New(w WhisperService) *Service {
return &Service{w: w}
}
// Protocols returns a new protocols list. In this case, there are none.
func (s *Service) Protocols() []p2p.Protocol {
return []p2p.Protocol{}
}
// APIs returns a list of new APIs.
func (s *Service) APIs() []rpc.API {
return []rpc.API{
{
Namespace: "status",
Version: "1.0",
Service: NewAPI(s),
Public: false,
},
}
}
// SetAccountManager sets account manager for the API calls.
func (s *Service) SetAccountManager(a AccountManager) {
s.am = a
}
// Start is run when a service is started.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Start(server *p2p.Server) error {
return nil
}
// Stop is run when a service is stopped.
// It does nothing in this case but is required by `node.Service` interface.
func (s *Service) Stop() error {
return nil
}

View File

@ -0,0 +1,106 @@
package services
import (
"encoding/json"
"fmt"
"github.com/status-im/status-go/geth/api"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/signal"
"github.com/status-im/status-go/t/e2e"
. "github.com/status-im/status-go/t/utils"
)
type BaseJSONRPCSuite struct {
e2e.BackendTestSuite
}
func (s *BaseJSONRPCSuite) AssertAPIMethodUnexported(method string) {
exported := s.isMethodExported(method, false)
s.False(exported,
"method %s should be hidden, but it isn't",
method)
}
func (s *BaseJSONRPCSuite) AssertAPIMethodExported(method string) {
exported := s.isMethodExported(method, false)
s.True(exported,
"method %s should be exported, but it isn't",
method)
}
func (s *BaseJSONRPCSuite) AssertAPIMethodExportedPrivately(method string) {
exported := s.isMethodExported(method, true)
s.True(exported,
"method %s should be exported, but it isn't",
method)
}
func (s *BaseJSONRPCSuite) isMethodExported(method string, private bool) bool {
var result string
cmd := fmt.Sprintf(`{"jsonrpc":"2.0", "method": "%s", "params": []}`, method)
if private {
result = s.Backend.CallPrivateRPC(cmd)
} else {
result = s.Backend.CallRPC(cmd)
}
var response struct {
Error *rpcError `json:"error"`
}
s.NoError(json.Unmarshal([]byte(result), &response))
return !(response.Error != nil && response.Error.Code == methodNotFoundErrorCode)
}
func (s *BaseJSONRPCSuite) SetupTest(upstreamEnabled bool, statusServiceEnabled bool) error {
s.Backend = api.NewStatusBackend()
s.NotNil(s.Backend)
nodeConfig, err := MakeTestNodeConfig(GetNetworkID())
s.NoError(err)
nodeConfig.IPCEnabled = false
nodeConfig.StatusServiceEnabled = statusServiceEnabled
nodeConfig.HTTPHost = "" // to make sure that no HTTP interface is started
if upstreamEnabled {
networkURL, err := GetRemoteURL()
s.NoError(err)
nodeConfig.UpstreamConfig.Enabled = true
nodeConfig.UpstreamConfig.URL = networkURL
}
return s.Backend.StartNode(nodeConfig)
}
func (s *BaseJSONRPCSuite) notificationHandler(account string, pass string, expectedError error) func(string) {
return func(jsonEvent string) {
envelope := unmarshalEnvelope(jsonEvent)
if envelope.Type == signal.EventSignRequestAdded {
event := envelope.Event.(map[string]interface{})
id := event["id"].(string)
s.T().Logf("Sign request added (will be completed shortly): {id: %s}\n", id)
//check for the correct method name
method := event["method"].(string)
s.Equal(params.PersonalSignMethodName, method)
//check the event data
args := event["args"].(map[string]interface{})
s.Equal(signDataString, args["data"].(string))
s.Equal(account, args["account"].(string))
e := s.Backend.ApproveSignRequest(id, pass).Error
s.T().Logf("Sign request approved. {id: %s, acc: %s, err: %v}", id, account, e)
if expectedError == nil {
s.NoError(e, "cannot complete sign reauest[%v]: %v", id, e)
} else {
s.EqualError(e, expectedError.Error())
}
}
}
}

View File

@ -11,7 +11,6 @@ import (
"github.com/status-im/status-go/geth/params" "github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/signal" "github.com/status-im/status-go/signal"
e2e "github.com/status-im/status-go/t/e2e"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
. "github.com/status-im/status-go/t/utils" . "github.com/status-im/status-go/t/utils"
@ -52,7 +51,7 @@ func TestPersonalSignSuiteUpstream(t *testing.T) {
} }
type PersonalSignSuite struct { type PersonalSignSuite struct {
e2e.BackendTestSuite BaseJSONRPCSuite
upstream bool upstream bool
} }
@ -62,40 +61,22 @@ func (s *PersonalSignSuite) TestRestrictedPersonalAPIs() {
return return
} }
err := s.initTest(s.upstream) err := s.SetupTest(s.upstream, false)
s.NoError(err) s.NoError(err)
defer func() { defer func() {
err := s.Backend.StopNode() err := s.Backend.StopNode()
s.NoError(err) s.NoError(err)
}() }()
// These personal APIs should be available // These personal APIs should be available
s.testAPIExported("personal_sign", true) s.AssertAPIMethodExported("personal_sign")
s.testAPIExported("personal_ecRecover", true) s.AssertAPIMethodExported("personal_ecRecover")
// These personal APIs shouldn't be exported // These personal APIs shouldn't be exported
s.testAPIExported("personal_sendTransaction", false) s.AssertAPIMethodUnexported("personal_sendTransaction")
s.testAPIExported("personal_unlockAccount", false) s.AssertAPIMethodUnexported("personal_unlockAccount")
s.testAPIExported("personal_newAccount", false) s.AssertAPIMethodUnexported("personal_newAccount")
s.testAPIExported("personal_lockAccount", false) s.AssertAPIMethodUnexported("personal_lockAccount")
s.testAPIExported("personal_listAccounts", false) s.AssertAPIMethodUnexported("personal_listAccounts")
s.testAPIExported("personal_importRawKey", false) s.AssertAPIMethodUnexported("personal_importRawKey")
}
func (s *PersonalSignSuite) testAPIExported(method string, expectExported bool) {
cmd := fmt.Sprintf(`{"jsonrpc":"2.0", "method": "%s", "params": []}`, method)
result := s.Backend.CallRPC(cmd)
var response struct {
Error *rpcError `json:"error"`
}
s.NoError(json.Unmarshal([]byte(result), &response))
hidden := (response.Error != nil && response.Error.Code == methodNotFoundErrorCode)
s.Equal(expectExported, !hidden,
"method %s should be %s, but it isn't",
method, map[bool]string{true: "exported", false: "hidden"}[expectExported])
} }
func (s *PersonalSignSuite) TestPersonalSignSuccess() { func (s *PersonalSignSuite) TestPersonalSignSuccess() {
@ -141,12 +122,6 @@ func (s *PersonalSignSuite) TestPersonalSignNoAccountSelected() {
} }
// Utility methods // Utility methods
func (s *PersonalSignSuite) notificationHandlerSuccess(account string, pass string) func(string) {
return func(jsonEvent string) {
s.notificationHandler(account, pass, nil)(jsonEvent)
}
}
func (s *PersonalSignSuite) notificationHandlerWrongPassword(account string, pass string) func(string) { func (s *PersonalSignSuite) notificationHandlerWrongPassword(account string, pass string) func(string) {
return func(jsonEvent string) { return func(jsonEvent string) {
s.notificationHandler(account, pass+"wrong", keystore.ErrDecrypt)(jsonEvent) s.notificationHandler(account, pass+"wrong", keystore.ErrDecrypt)(jsonEvent)
@ -216,7 +191,7 @@ func (s *PersonalSignSuite) testPersonalSign(testParams testParams) string {
testParams.HandlerFactory = s.notificationHandlerSuccess testParams.HandlerFactory = s.notificationHandlerSuccess
} }
err := s.initTest(s.upstream) err := s.SetupTest(s.upstream, false)
s.NoError(err) s.NoError(err)
defer func() { defer func() {
err := s.Backend.StopNode() err := s.Backend.StopNode()
@ -246,6 +221,15 @@ func (s *PersonalSignSuite) testPersonalSign(testParams testParams) string {
return "" return ""
} }
func (s *PersonalSignSuite) extractResultFromRPCResponse(response string) string {
var r struct {
Result string `json:"result"`
}
s.NoError(json.Unmarshal([]byte(response), &r))
return r.Result
}
func unmarshalEnvelope(jsonEvent string) signal.Envelope { func unmarshalEnvelope(jsonEvent string) signal.Envelope {
var envelope signal.Envelope var envelope signal.Envelope
if e := json.Unmarshal([]byte(jsonEvent), &envelope); e != nil { if e := json.Unmarshal([]byte(jsonEvent), &envelope); e != nil {
@ -268,7 +252,7 @@ func (s *PersonalSignSuite) TestPersonalRecoverSuccess() {
return return
} }
err := s.initTest(s.upstream) err := s.SetupTest(s.upstream, false)
s.NoError(err) s.NoError(err)
defer func() { defer func() {
err := s.Backend.StopNode() err := s.Backend.StopNode()
@ -288,29 +272,8 @@ func (s *PersonalSignSuite) TestPersonalRecoverSuccess() {
s.True(strings.EqualFold(result, TestConfig.Account1.Address)) s.True(strings.EqualFold(result, TestConfig.Account1.Address))
} }
func (s *PersonalSignSuite) initTest(upstreamEnabled bool) error { func (s *BaseJSONRPCSuite) notificationHandlerSuccess(account string, pass string) func(string) {
nodeConfig, err := MakeTestNodeConfig(GetNetworkID()) return func(jsonEvent string) {
s.NoError(err) s.notificationHandler(account, pass, nil)(jsonEvent)
nodeConfig.IPCEnabled = false
nodeConfig.HTTPHost = "" // to make sure that no HTTP interface is started
if upstreamEnabled {
networkURL, err := GetRemoteURL()
s.NoError(err)
nodeConfig.UpstreamConfig.Enabled = true
nodeConfig.UpstreamConfig.URL = networkURL
} }
return s.Backend.StartNode(nodeConfig)
}
func (s *PersonalSignSuite) extractResultFromRPCResponse(response string) string {
var r struct {
Result string `json:"result"`
}
s.NoError(json.Unmarshal([]byte(response), &r))
return r.Result
} }

View File

@ -0,0 +1,193 @@
package services
import (
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/status-im/status-go/geth/account"
"github.com/status-im/status-go/geth/params"
"github.com/status-im/status-go/services/status"
"github.com/status-im/status-go/signal"
"github.com/stretchr/testify/suite"
. "github.com/status-im/status-go/t/utils"
)
type statusTestParams struct {
Address string
Password string
HandlerFactory func(string, string) func(string)
ExpectedError error
ChannelName string
}
func TestStatusAPISuite(t *testing.T) {
s := new(StatusAPISuite)
s.upstream = false
suite.Run(t, s)
}
func TestStatusAPISuiteUpstream(t *testing.T) {
s := new(StatusAPISuite)
s.upstream = true
suite.Run(t, s)
}
type StatusAPISuite struct {
BaseJSONRPCSuite
upstream bool
}
func (s *StatusAPISuite) TestAccessibleStatusAPIs() {
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
s.T().Skip()
return
}
err := s.SetupTest(s.upstream, true)
s.NoError(err)
defer func() {
err := s.Backend.StopNode()
s.NoError(err)
}()
// These status APIs should be unavailable
s.AssertAPIMethodUnexported("status_login")
s.AssertAPIMethodUnexported("status_signup")
// These status APIs should be available only for IPC
s.AssertAPIMethodExportedPrivately("status_login")
s.AssertAPIMethodExportedPrivately("status_signup")
}
func (s *StatusAPISuite) TestStatusLoginSuccess() {
addressKeyID := s.testStatusLogin(statusTestParams{
Address: TestConfig.Account1.Address,
Password: TestConfig.Account1.Password,
})
s.NotEmpty(addressKeyID)
}
func (s *StatusAPISuite) TestStatusLoginInvalidAddress() {
s.testStatusLogin(statusTestParams{
Address: "invalidaccount",
Password: TestConfig.Account1.Password,
ExpectedError: account.ErrAddressToAccountMappingFailure,
})
}
func (s *StatusAPISuite) TestStatusLoginInvalidPassword() {
s.testStatusLogin(statusTestParams{
Address: "invalidaccount",
Password: TestConfig.Account1.Password,
ExpectedError: account.ErrAddressToAccountMappingFailure,
})
}
func (s *StatusAPISuite) TestStatusSignupSuccess() {
var pwd = "randompassword"
res := s.testStatusSignup(statusTestParams{
Password: pwd,
})
s.NotEmpty(res.Address)
s.NotEmpty(res.Pubkey)
s.Equal(12, len(strings.Split(res.Mnemonic, " ")))
// I should be able to login with the newly created account
_ = s.testStatusLogin(statusTestParams{
Address: res.Address,
Password: pwd,
})
}
func (s *StatusAPISuite) testStatusLogin(testParams statusTestParams) *status.LoginResponse {
// Test upstream if that's not StatusChain
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
s.T().Skip()
return nil
}
if testParams.HandlerFactory == nil {
testParams.HandlerFactory = s.notificationHandlerSuccess
}
err := s.SetupTest(s.upstream, true)
s.NoError(err)
defer func() {
err := s.Backend.StopNode()
s.NoError(err)
}()
signal.SetDefaultNodeNotificationHandler(testParams.HandlerFactory(testParams.Address, testParams.Password))
req := status.LoginRequest{
Addr: testParams.Address,
Password: testParams.Password,
}
body, _ := json.Marshal(req)
basicCall := fmt.Sprintf(
`{"jsonrpc":"2.0","method":"status_login","params":[%s],"id":67}`,
body)
result := s.Backend.CallPrivateRPC(basicCall)
if testParams.ExpectedError == nil {
s.NotContains(result, "error")
var r struct {
Result *status.LoginResponse `json:"result"`
}
s.NoError(json.Unmarshal([]byte(result), &r))
return r.Result
}
s.Contains(result, testParams.ExpectedError.Error())
return nil
}
func (s *StatusAPISuite) testStatusSignup(testParams statusTestParams) *status.SignupResponse {
// Test upstream if that's not StatusChain
if s.upstream && GetNetworkID() == params.StatusChainNetworkID {
s.T().Skip()
return nil
}
if testParams.HandlerFactory == nil {
testParams.HandlerFactory = s.notificationHandlerSuccess
}
err := s.SetupTest(s.upstream, true)
s.NoError(err)
defer func() {
err := s.Backend.StopNode()
s.NoError(err)
}()
signal.SetDefaultNodeNotificationHandler(testParams.HandlerFactory(testParams.Address, testParams.Password))
req := status.SignupRequest{
Password: testParams.Password,
}
body, _ := json.Marshal(req)
basicCall := fmt.Sprintf(
`{"jsonrpc":"2.0","method":"status_signup","params":[%s],"id":67}`,
body)
result := s.Backend.CallPrivateRPC(basicCall)
if testParams.ExpectedError == nil {
s.NotContains(result, "error")
var r struct {
Result *status.SignupResponse `json:"result"`
}
s.NoError(json.Unmarshal([]byte(result), &r))
return r.Result
}
s.Contains(result, testParams.ExpectedError.Error())
return nil
}