From c10154d947fd8dd50e0503fde714e52862ba6d60 Mon Sep 17 00:00:00 2001 From: manoranjith Date: Fri, 18 Dec 2020 20:29:30 +0530 Subject: [PATCH] Add unit tests, improve integ tests in session pkg (#142) - Add unit tests for all methods of Session and Channel types. For make it feasible to tests, add a few interfaces. - Improve the integration tests used for client initialization to increase coverage. Closes #131. Signed-off-by: Manoranjith --- client/client.go | 35 +- client/client_integ_test.go | 4 +- client/client_test.go | 6 +- internal/mocks/ChClient.go | 24 +- internal/mocks/ChProposalResponder.go | 55 ++ internal/mocks/ChUpdateResponder.go | 42 ++ internal/mocks/Channel.go | 199 +++++ internal/mocks/ChannelProposal.go | 76 ++ perun.go | 36 +- session/channel.go | 84 ++- session/channel_internal_test.go | 29 - session/channel_test.go | 541 ++++++++++++++ session/export_test.go | 61 +- session/session.go | 103 ++- session/session_integ_test.go | 68 +- session/session_internal_test.go | 29 - session/session_test.go | 694 ++++++++++++++++++ .../persistence/alice-database/000001.log | Bin 5328 -> 6056 bytes .../session/persistence/alice-database/LOG | 16 +- 19 files changed, 1897 insertions(+), 205 deletions(-) create mode 100644 internal/mocks/ChProposalResponder.go create mode 100644 internal/mocks/ChUpdateResponder.go create mode 100644 internal/mocks/Channel.go create mode 100644 internal/mocks/ChannelProposal.go delete mode 100644 session/channel_internal_test.go create mode 100644 session/channel_test.go delete mode 100644 session/session_internal_test.go create mode 100644 session/session_test.go diff --git a/client/client.go b/client/client.go index af872db9..2ccc46b9 100644 --- a/client/client.go +++ b/client/client.go @@ -60,18 +60,43 @@ type client struct { // pClient represents the methods on client.Client that are used by client. type pClient interface { - ProposeChannel(context.Context, pclient.ChannelProposal) (*pclient.Channel, error) + ProposeChannel(context.Context, pclient.ChannelProposal) (perun.Channel, error) Handle(pclient.ProposalHandler, pclient.UpdateHandler) - Channel(pchannel.ID) (*pclient.Channel, error) + Channel(pchannel.ID) (perun.Channel, error) Close() error EnablePersistence(ppersistence.PersistRestorer) - OnNewChannel(handler func(*pclient.Channel)) + OnNewChannel(handler func(perun.Channel)) Restore(context.Context) error Log() plog.Logger } +// pclientWrapped is a wrapper around pclient.Client that returns a channel of interface type +// instead of struct type. This enables easier mocking of the returned value in tests. +type pclientWrapped struct { + *pclient.Client +} + +// ProposeChannel is a wrapper around the original function, that returns a channel of interface type instead of +// struct type. +func (c *pclientWrapped) ProposeChannel(ctx context.Context, proposal pclient.ChannelProposal) (perun.Channel, error) { + return c.Client.ProposeChannel(ctx, proposal) +} + +// Channel is a wrapper around the original function, that returns a channel of interface type instead of struct type. +func (c *pclientWrapped) Channel(id pchannel.ID) (perun.Channel, error) { + return c.Client.Channel(id) +} + +// OnNewChannel is a wrapper around the original function, that takes a handler that takes channel of interface type as +// argument instead of the handler in original function that takes channel of struct type as argument. +func (c *pclientWrapped) OnNewChannel(handler func(perun.Channel)) { + c.Client.OnNewChannel(func(ch *pclient.Channel) { + handler(ch) + }) +} + // NewEthereumPaymentClient initializes a two party, ethereum payment channel client for the given user. // It establishes a connection to the blockchain and verifies the integrity of contracts at the given address. // It uses the comm backend to initialize adapters for off-chain communication network. @@ -93,7 +118,7 @@ func NewEthereumPaymentClient(cfg Config, user perun.User, comm perun.CommBacken } c := &client{ - pClient: pcClient, + pClient: &pclientWrapped{pcClient}, msgBus: msgBus, msgBusRegistry: dialer, dbPath: cfg.DatabaseDir, @@ -123,7 +148,7 @@ func (c *client) Handle(ph pclient.ProposalHandler, ch pclient.UpdateHandler) { // RestoreChs will restore the persisted channels. Register OnNewChannel Callback // before calling this function. -func (c *client) RestoreChs(handler func(*pclient.Channel)) error { +func (c *client) RestoreChs(handler func(perun.Channel)) error { c.OnNewChannel(handler) db, err := pleveldb.LoadDatabase(c.dbPath) if err != nil { diff --git a/client/client_integ_test.go b/client/client_integ_test.go index 307b813c..b2d51e31 100644 --- a/client/client_integ_test.go +++ b/client/client_integ_test.go @@ -28,8 +28,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - pclient "perun.network/go-perun/client" + "github.com/hyperledger-labs/perun-node" "github.com/hyperledger-labs/perun-node/blockchain/ethereum/ethereumtest" "github.com/hyperledger-labs/perun-node/client" "github.com/hyperledger-labs/perun-node/client/clienttest" @@ -73,7 +73,7 @@ func Test_Integ_NewEthereumPaymentClient(t *testing.T) { cfg.DatabaseDir = newDatabaseDir(t) // start with empty persistence dir each time. client, err := client.NewEthereumPaymentClient(cfg, user, tcp.NewTCPBackend(tcptest.DialerTimeout)) require.NoError(t, err) - err = client.RestoreChs(func(*pclient.Channel) {}) + err = client.RestoreChs(func(perun.Channel) {}) assert.NoError(t, err) assert.NoError(t, client.Close()) }) diff --git a/client/client_test.go b/client/client_test.go index 129d2f9c..c3c89cde 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -56,10 +56,10 @@ func Test_Client_Register(t *testing.T) { // happy path test is covered in integration test, as internal components of // the client should be initialized. t.Run("happy", func(t *testing.T) { - registere := &mocks.Registerer{} + registerer := &mocks.Registerer{} dbConnCloser := &mocks.Closer{} - Client := client.NewClientForTest(nil, nil, registere, dbConnCloser) - registere.On("Register", nil, "").Return() + Client := client.NewClientForTest(nil, nil, registerer, dbConnCloser) + registerer.On("Register", nil, "").Return() Client.Register(nil, "") }) } diff --git a/internal/mocks/ChClient.go b/internal/mocks/ChClient.go index 731f428c..c0515361 100644 --- a/internal/mocks/ChClient.go +++ b/internal/mocks/ChClient.go @@ -13,6 +13,8 @@ import ( persistence "perun.network/go-perun/channel/persistence" + perun "github.com/hyperledger-labs/perun-node" + wallet "perun.network/go-perun/wallet" ) @@ -22,15 +24,15 @@ type ChClient struct { } // Channel provides a mock function with given fields: _a0 -func (_m *ChClient) Channel(_a0 [32]byte) (*client.Channel, error) { +func (_m *ChClient) Channel(_a0 [32]byte) (perun.Channel, error) { ret := _m.Called(_a0) - var r0 *client.Channel - if rf, ok := ret.Get(0).(func([32]byte) *client.Channel); ok { + var r0 perun.Channel + if rf, ok := ret.Get(0).(func([32]byte) perun.Channel); ok { r0 = rf(_a0) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*client.Channel) + r0 = ret.Get(0).(perun.Channel) } } @@ -85,20 +87,20 @@ func (_m *ChClient) Log() log.Logger { } // OnNewChannel provides a mock function with given fields: handler -func (_m *ChClient) OnNewChannel(handler func(*client.Channel)) { +func (_m *ChClient) OnNewChannel(handler func(perun.Channel)) { _m.Called(handler) } // ProposeChannel provides a mock function with given fields: _a0, _a1 -func (_m *ChClient) ProposeChannel(_a0 context.Context, _a1 client.ChannelProposal) (*client.Channel, error) { +func (_m *ChClient) ProposeChannel(_a0 context.Context, _a1 client.ChannelProposal) (perun.Channel, error) { ret := _m.Called(_a0, _a1) - var r0 *client.Channel - if rf, ok := ret.Get(0).(func(context.Context, client.ChannelProposal) *client.Channel); ok { + var r0 perun.Channel + if rf, ok := ret.Get(0).(func(context.Context, client.ChannelProposal) perun.Channel); ok { r0 = rf(_a0, _a1) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*client.Channel) + r0 = ret.Get(0).(perun.Channel) } } @@ -132,11 +134,11 @@ func (_m *ChClient) Restore(_a0 context.Context) error { } // RestoreChs provides a mock function with given fields: _a0 -func (_m *ChClient) RestoreChs(_a0 func(*client.Channel)) error { +func (_m *ChClient) RestoreChs(_a0 func(perun.Channel)) error { ret := _m.Called(_a0) var r0 error - if rf, ok := ret.Get(0).(func(func(*client.Channel)) error); ok { + if rf, ok := ret.Get(0).(func(func(perun.Channel)) error); ok { r0 = rf(_a0) } else { r0 = ret.Error(0) diff --git a/internal/mocks/ChProposalResponder.go b/internal/mocks/ChProposalResponder.go new file mode 100644 index 00000000..477a6994 --- /dev/null +++ b/internal/mocks/ChProposalResponder.go @@ -0,0 +1,55 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + client "perun.network/go-perun/client" + + mock "github.com/stretchr/testify/mock" + + perun "github.com/hyperledger-labs/perun-node" +) + +// ChProposalResponder is an autogenerated mock type for the ChProposalResponder type +type ChProposalResponder struct { + mock.Mock +} + +// Accept provides a mock function with given fields: _a0, _a1 +func (_m *ChProposalResponder) Accept(_a0 context.Context, _a1 *client.ChannelProposalAcc) (perun.Channel, error) { + ret := _m.Called(_a0, _a1) + + var r0 perun.Channel + if rf, ok := ret.Get(0).(func(context.Context, *client.ChannelProposalAcc) perun.Channel); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(perun.Channel) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *client.ChannelProposalAcc) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Reject provides a mock function with given fields: ctx, reason +func (_m *ChProposalResponder) Reject(ctx context.Context, reason string) error { + ret := _m.Called(ctx, reason) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, reason) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/internal/mocks/ChUpdateResponder.go b/internal/mocks/ChUpdateResponder.go new file mode 100644 index 00000000..a4b4a27c --- /dev/null +++ b/internal/mocks/ChUpdateResponder.go @@ -0,0 +1,42 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// ChUpdateResponder is an autogenerated mock type for the ChUpdateResponder type +type ChUpdateResponder struct { + mock.Mock +} + +// Accept provides a mock function with given fields: ctx +func (_m *ChUpdateResponder) Accept(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Reject provides a mock function with given fields: ctx, reason +func (_m *ChUpdateResponder) Reject(ctx context.Context, reason string) error { + ret := _m.Called(ctx, reason) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, reason) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/internal/mocks/Channel.go b/internal/mocks/Channel.go new file mode 100644 index 00000000..d093ad2c --- /dev/null +++ b/internal/mocks/Channel.go @@ -0,0 +1,199 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + context "context" + + channel "perun.network/go-perun/channel" + + mock "github.com/stretchr/testify/mock" + + wallet "perun.network/go-perun/wallet" +) + +// Channel is an autogenerated mock type for the Channel type +type Channel struct { + mock.Mock +} + +// Close provides a mock function with given fields: +func (_m *Channel) Close() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ID provides a mock function with given fields: +func (_m *Channel) ID() [32]byte { + ret := _m.Called() + + var r0 [32]byte + if rf, ok := ret.Get(0).(func() [32]byte); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([32]byte) + } + } + + return r0 +} + +// Idx provides a mock function with given fields: +func (_m *Channel) Idx() uint16 { + ret := _m.Called() + + var r0 uint16 + if rf, ok := ret.Get(0).(func() uint16); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint16) + } + + return r0 +} + +// IsClosed provides a mock function with given fields: +func (_m *Channel) IsClosed() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// OnUpdate provides a mock function with given fields: cb +func (_m *Channel) OnUpdate(cb func(*channel.State, *channel.State)) { + _m.Called(cb) +} + +// Params provides a mock function with given fields: +func (_m *Channel) Params() *channel.Params { + ret := _m.Called() + + var r0 *channel.Params + if rf, ok := ret.Get(0).(func() *channel.Params); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*channel.Params) + } + } + + return r0 +} + +// Peers provides a mock function with given fields: +func (_m *Channel) Peers() []wallet.Address { + ret := _m.Called() + + var r0 []wallet.Address + if rf, ok := ret.Get(0).(func() []wallet.Address); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]wallet.Address) + } + } + + return r0 +} + +// Phase provides a mock function with given fields: +func (_m *Channel) Phase() channel.Phase { + ret := _m.Called() + + var r0 channel.Phase + if rf, ok := ret.Get(0).(func() channel.Phase); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(channel.Phase) + } + + return r0 +} + +// Settle provides a mock function with given fields: ctx +func (_m *Channel) Settle(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SettleSecondary provides a mock function with given fields: ctx +func (_m *Channel) SettleSecondary(ctx context.Context) error { + ret := _m.Called(ctx) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// State provides a mock function with given fields: +func (_m *Channel) State() *channel.State { + ret := _m.Called() + + var r0 *channel.State + if rf, ok := ret.Get(0).(func() *channel.State); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*channel.State) + } + } + + return r0 +} + +// UpdateBy provides a mock function with given fields: ctx, update +func (_m *Channel) UpdateBy(ctx context.Context, update func(*channel.State)) error { + ret := _m.Called(ctx, update) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, func(*channel.State)) error); ok { + r0 = rf(ctx, update) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Watch provides a mock function with given fields: +func (_m *Channel) Watch() error { + ret := _m.Called() + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/internal/mocks/ChannelProposal.go b/internal/mocks/ChannelProposal.go new file mode 100644 index 00000000..893b3110 --- /dev/null +++ b/internal/mocks/ChannelProposal.go @@ -0,0 +1,76 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + io "io" + + client "perun.network/go-perun/client" + + mock "github.com/stretchr/testify/mock" + + wire "perun.network/go-perun/wire" +) + +// ChannelProposal is an autogenerated mock type for the ChannelProposal type +type ChannelProposal struct { + mock.Mock +} + +// Decode provides a mock function with given fields: _a0 +func (_m *ChannelProposal) Decode(_a0 io.Reader) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(io.Reader) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Encode provides a mock function with given fields: _a0 +func (_m *ChannelProposal) Encode(_a0 io.Writer) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(io.Writer) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Proposal provides a mock function with given fields: +func (_m *ChannelProposal) Proposal() *client.BaseChannelProposal { + ret := _m.Called() + + var r0 *client.BaseChannelProposal + if rf, ok := ret.Get(0).(func() *client.BaseChannelProposal); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.BaseChannelProposal) + } + } + + return r0 +} + +// Type provides a mock function with given fields: +func (_m *ChannelProposal) Type() wire.Type { + ret := _m.Called() + + var r0 wire.Type + if rf, ok := ret.Get(0).(func() wire.Type); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(wire.Type) + } + + return r0 +} diff --git a/perun.go b/perun.go index 048cd500..9f4fea0b 100644 --- a/perun.go +++ b/perun.go @@ -127,6 +127,25 @@ type Session struct { ChClient ChClient } +//go:generate mockery --name Channel --output ./internal/mocks + +// Channel represents state channel established among the participants of the off-chain network. +type Channel interface { + Close() error + ID() pchannel.ID + Idx() pchannel.Index + IsClosed() bool + Params() *pchannel.Params + Peers() []pwire.Address + Phase() pchannel.Phase + State() *pchannel.State + OnUpdate(cb func(from, to *pchannel.State)) + UpdateBy(ctx context.Context, update func(*pchannel.State)) error + Settle(ctx context.Context) error + SettleSecondary(ctx context.Context) error + Watch() error +} + //go:generate mockery --name ChClient --output ./internal/mocks // ChClient allows the user to establish off-chain channels and transact on these channels. @@ -139,15 +158,15 @@ type Session struct { // Hence it is highly recommended not to stop the channel client if there are open channels. type ChClient interface { Registerer - ProposeChannel(context.Context, pclient.ChannelProposal) (*pclient.Channel, error) + ProposeChannel(context.Context, pclient.ChannelProposal) (Channel, error) Handle(pclient.ProposalHandler, pclient.UpdateHandler) - Channel(pchannel.ID) (*pclient.Channel, error) + Channel(pchannel.ID) (Channel, error) Close() error EnablePersistence(ppersistence.PersistRestorer) - OnNewChannel(handler func(*pclient.Channel)) + OnNewChannel(handler func(Channel)) Restore(context.Context) error - RestoreChs(func(*pclient.Channel)) error + RestoreChs(func(Channel)) error Log() pLog.Logger } @@ -256,15 +275,6 @@ type ( ChallengeDurSecs uint64 Expiry int64 } - - // ChCloseNotifier is the notifier function that is used for sending channel close notifications. - ChCloseNotifier func(ChCloseNotif) - - // ChCloseNotif represents the parameters sent in a channel close notifications. - ChCloseNotif struct { - ClosedChInfo ChInfo - Error string - } ) //go:generate mockery --name ChAPI --output ./internal/mocks diff --git a/session/channel.go b/session/channel.go index f34be12a..9f219a72 100644 --- a/session/channel.go +++ b/session/channel.go @@ -41,11 +41,12 @@ const ( ) type ( - channel struct { + // Channel implements perun.ChAPI. + Channel struct { log.Logger id string - pch *pclient.Channel + pch perun.Channel status chStatus currency string parts []string @@ -65,23 +66,23 @@ type ( chUpdateResponderEntry struct { notif perun.ChUpdateNotif - responder chUpdateResponder + responder ChUpdateResponder notifExpiry int64 } - //go:generate mockery --name ProposalResponder --output ../internal/mocks - - // ChUpdaterResponder represents the methods on channel update responder that will be used the perun node. - chUpdateResponder interface { + // ChUpdateResponder represents the methods on channel update responder that will be used the perun node. + ChUpdateResponder interface { Accept(ctx context.Context) error Reject(ctx context.Context, reason string) error } ) +//go:generate mockery --name ChUpdateResponder --output ../internal/mocks + // newCh sets up a channel object from the passed pchannel. -func newCh(pch *pclient.Channel, currency string, parts []string, timeoutCfg timeoutConfig, - challengeDurSecs uint64) *channel { - ch := &channel{ +func newCh(pch perun.Channel, currency string, parts []string, timeoutCfg timeoutConfig, + challengeDurSecs uint64) *Channel { + ch := &Channel{ id: fmt.Sprintf("%x", pch.ID()), pch: pch, status: open, @@ -94,7 +95,7 @@ func newCh(pch *pclient.Channel, currency string, parts []string, timeoutCfg tim watcherWg: &sync.WaitGroup{}, } ch.watcherWg.Add(1) - go func(ch *channel) { + go func(ch *Channel) { err := ch.pch.Watch() ch.watcherWg.Done() @@ -103,24 +104,24 @@ func newCh(pch *pclient.Channel, currency string, parts []string, timeoutCfg tim return ch } -// ID() returns the ID of the channel. +// ID returns the ID of the channel. // // Does not require a mutex lock, as the data will remain unchanged throughout the lifecycle of the channel. -func (ch *channel) ID() string { +func (ch *Channel) ID() string { return ch.id } // Currency returns the currency interpreter used in the channel. // // Does not require a mutex lock, as the data will remain unchanged throughout the lifecycle of the channel. -func (ch *channel) Currency() string { +func (ch *Channel) Currency() string { return ch.currency } // Parts returns the list of aliases of the channel participants. // // Does not require a mutex lock, as the data will remain unchanged throughout the lifecycle of the channel. -func (ch *channel) Parts() []string { +func (ch *Channel) Parts() []string { return ch.parts } @@ -128,11 +129,12 @@ func (ch *channel) Parts() []string { // an invalid/older state is registered on the blockchain closing the channel. // // Does not require a mutex lock, as the data will remain unchanged throughout the lifecycle of the channel. -func (ch *channel) ChallengeDurSecs() uint64 { +func (ch *Channel) ChallengeDurSecs() uint64 { return ch.challengeDurSecs } -func (ch *channel) SendChUpdate(pctx context.Context, updater perun.StateUpdater) (perun.ChInfo, error) { +// SendChUpdate implements chAPI.SendChUpdate. +func (ch *Channel) SendChUpdate(pctx context.Context, updater perun.StateUpdater) (perun.ChInfo, error) { ch.Debug("Received request: channel.SendChUpdate") ch.Lock() defer ch.Unlock() @@ -151,7 +153,7 @@ func (ch *channel) SendChUpdate(pctx context.Context, updater perun.StateUpdater return ch.getChInfo(), nil } -func (ch *channel) sendChUpdate(pctx context.Context, updater perun.StateUpdater) error { +func (ch *Channel) sendChUpdate(pctx context.Context, updater perun.StateUpdater) error { ctx, cancel := context.WithTimeout(pctx, ch.timeoutCfg.chUpdate()) defer cancel() err := ch.pch.UpdateBy(ctx, updater) @@ -165,7 +167,10 @@ func (ch *channel) sendChUpdate(pctx context.Context, updater perun.StateUpdater return perun.GetAPIError(err) } -func (ch *channel) HandleUpdate(chUpdate pclient.ChannelUpdate, responder *pclient.UpdateResponder) { +// HandleUpdate handles the incoming updates on an open channel. All updates are sent to a centralized +// update handler defined on the session. The centrazlied handler identifies the channel and then +// invokes this function to process the update. +func (ch *Channel) HandleUpdate(chUpdate pclient.ChannelUpdate, responder ChUpdateResponder) { ch.Lock() defer ch.Unlock() @@ -190,7 +195,7 @@ func (ch *channel) HandleUpdate(chUpdate pclient.ChannelUpdate, responder *pclie ch.sendChUpdateNotif(notif) } -func (ch *channel) sendChUpdateNotif(notif perun.ChUpdateNotif) { +func (ch *Channel) sendChUpdateNotif(notif perun.ChUpdateNotif) { if ch.chUpdateNotifier == nil { ch.chUpdateNotifCache = append(ch.chUpdateNotifCache, notif) ch.Debug("HandleUpdate: Notification cached") @@ -220,7 +225,8 @@ func makeChUpdateNotif(currChInfo perun.ChInfo, proposedState *pchannel.State, e } } -func (ch *channel) SubChUpdates(notifier perun.ChUpdateNotifier) error { +// SubChUpdates implements chAPI.SubChUpdates. +func (ch *Channel) SubChUpdates(notifier perun.ChUpdateNotifier) error { ch.Debug("Received request: channel.SubChUpdates") ch.Lock() defer ch.Unlock() @@ -243,7 +249,8 @@ func (ch *channel) SubChUpdates(notifier perun.ChUpdateNotifier) error { return nil } -func (ch *channel) UnsubChUpdates() error { +// UnsubChUpdates implements chAPI.UnsubChUpdates. +func (ch *Channel) UnsubChUpdates() error { ch.Debug("Received request: channel.UnsubChUpdates") ch.Lock() defer ch.Unlock() @@ -260,11 +267,12 @@ func (ch *channel) UnsubChUpdates() error { return nil } -func (ch *channel) unsubChUpdates() { +func (ch *Channel) unsubChUpdates() { ch.chUpdateNotifier = nil } -func (ch *channel) RespondChUpdate(pctx context.Context, updateID string, accept bool) (perun.ChInfo, error) { +// RespondChUpdate implements chAPI.RespondChUpdate. +func (ch *Channel) RespondChUpdate(pctx context.Context, updateID string, accept bool) (perun.ChInfo, error) { ch.Debug("Received request channel.RespondChUpdate") ch.Lock() defer ch.Unlock() @@ -300,7 +308,7 @@ func (ch *channel) RespondChUpdate(pctx context.Context, updateID string, accept return ch.getChInfo(), err } -func (ch *channel) acceptChUpdate(pctx context.Context, entry chUpdateResponderEntry) error { +func (ch *Channel) acceptChUpdate(pctx context.Context, entry chUpdateResponderEntry) error { ctx, cancel := context.WithTimeout(pctx, ch.timeoutCfg.respChUpdate()) defer cancel() err := entry.responder.Accept(ctx) @@ -312,7 +320,7 @@ func (ch *channel) acceptChUpdate(pctx context.Context, entry chUpdateResponderE return perun.GetAPIError(errors.Wrap(err, "accepting update")) } -func (ch *channel) rejectChUpdate(pctx context.Context, entry chUpdateResponderEntry, reason string) error { +func (ch *Channel) rejectChUpdate(pctx context.Context, entry chUpdateResponderEntry, reason string) error { ctx, cancel := context.WithTimeout(pctx, ch.timeoutCfg.respChUpdate()) defer cancel() err := entry.responder.Reject(ctx, reason) @@ -322,7 +330,8 @@ func (ch *channel) rejectChUpdate(pctx context.Context, entry chUpdateResponderE return perun.GetAPIError(errors.Wrap(err, "rejecting update")) } -func (ch *channel) GetChInfo() perun.ChInfo { +// GetChInfo implements chAPI.GetChInfo. +func (ch *Channel) GetChInfo() perun.ChInfo { ch.Debug("Received request: channel.GetChInfo") ch.Lock() chInfo := ch.getChInfo() @@ -331,11 +340,14 @@ func (ch *channel) GetChInfo() perun.ChInfo { } // This function assumes that caller has already locked the channel. -func (ch *channel) getChInfo() perun.ChInfo { +func (ch *Channel) getChInfo() perun.ChInfo { return makeChInfo(ch.ID(), ch.parts, ch.currency, ch.currState) } func makeChInfo(chID string, parts []string, curr string, state *pchannel.State) perun.ChInfo { + if state == nil { + return perun.ChInfo{} + } return perun.ChInfo{ ChID: chID, BalInfo: makeBalInfoFromState(parts, curr, state), @@ -354,9 +366,6 @@ func makeApp(def pchannel.App, data pchannel.Data) perun.App { // makeBalInfoFromState retrieves balance information from the channel state. func makeBalInfoFromState(parts []string, curr string, state *pchannel.State) perun.BalInfo { - if state == nil { - return perun.BalInfo{} - } return makeBalInfoFromRawBal(parts, curr, state.Balances[0]) } @@ -381,7 +390,7 @@ func makeBalInfoFromRawBal(parts []string, curr string, rawBal []*big.Int) perun // Then it sends a channel close notification if the channel is already subscribed. // If the channel is not subscribed, notification will not be cached as it not possible for the user // to subscribe to channel after it is closed. -func (ch *channel) HandleWatcherReturned(err error) { +func (ch *Channel) HandleWatcherReturned(err error) { ch.Lock() defer ch.Unlock() ch.Debug("Watch returned") @@ -415,7 +424,8 @@ func makeChCloseNotif(currChInfo perun.ChInfo, err error) perun.ChUpdateNotif { } } -func (ch *channel) Close(pctx context.Context) (perun.ChInfo, error) { +// Close implements chAPI.Close. +func (ch *Channel) Close(pctx context.Context) (perun.ChInfo, error) { ch.Debug("Received request channel.Close") ch.Lock() defer ch.Unlock() @@ -436,7 +446,7 @@ func (ch *channel) Close(pctx context.Context) (perun.ChInfo, error) { // the channel on the blockchain without registering or waiting for challenge duration to expire. // If this fails, calling Settle consequently will close the channel non-collaboratively, by registering // the state on-chain and waiting for challenge duration to expire. -func (ch *channel) finalize(pctx context.Context) { +func (ch *Channel) finalize(pctx context.Context) { chFinalizer := func(state *pchannel.State) { state.IsFinal = true } @@ -451,7 +461,7 @@ func (ch *channel) finalize(pctx context.Context) { } // settlePrimary is used when the channel close initiated by the user. -func (ch *channel) settlePrimary(pctx context.Context) error { +func (ch *Channel) settlePrimary(pctx context.Context) error { // TODO (mano): Document what happens when a Settle fails, should channel close be called again ? ctx, cancel := context.WithTimeout(pctx, ch.timeoutCfg.settleChPrimary(ch.challengeDurSecs)) defer cancel() @@ -465,7 +475,7 @@ func (ch *channel) settlePrimary(pctx context.Context) error { } // settleSecondary is used when the channel close is initiated after accepting a final update. -func (ch *channel) settleSecondary(pctx context.Context) error { +func (ch *Channel) settleSecondary(pctx context.Context) error { // TODO (mano): Document what happens when a Settle fails, should channel close be called again ? ctx, cancel := context.WithTimeout(pctx, ch.timeoutCfg.settleChSecondary(ch.challengeDurSecs)) defer cancel() @@ -481,7 +491,7 @@ func (ch *channel) settleSecondary(pctx context.Context) error { // Close the computing resources (listeners, subscriptions etc.,) of the channel. // If it fails, this error can be ignored. // It also removes the channel from the session. -func (ch *channel) close() { +func (ch *Channel) close() { ch.watcherWg.Wait() if err := ch.pch.Close(); err != nil { diff --git a/session/channel_internal_test.go b/session/channel_internal_test.go deleted file mode 100644 index 07b833e9..00000000 --- a/session/channel_internal_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2020 - for information on the respective copyright owner -// see the NOTICE file and/or the repository at -// https://github.com/hyperledger-labs/perun-node -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package session - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/hyperledger-labs/perun-node" -) - -func Test_ChAPI_Interface(t *testing.T) { - assert.Implements(t, (*perun.ChAPI)(nil), new(channel)) -} diff --git a/session/channel_test.go b/session/channel_test.go new file mode 100644 index 00000000..c44c866c --- /dev/null +++ b/session/channel_test.go @@ -0,0 +1,541 @@ +// Copyright (c) 2020 - for information on the respective copyright owner +// see the NOTICE file and/or the repository at +// https://github.com/hyperledger-labs/perun-node +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package session_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + pchannel "perun.network/go-perun/channel" + pclient "perun.network/go-perun/client" + + "github.com/hyperledger-labs/perun-node" + "github.com/hyperledger-labs/perun-node/currency" + "github.com/hyperledger-labs/perun-node/internal/mocks" + "github.com/hyperledger-labs/perun-node/session" +) + +func Test_ChAPI_Interface(t *testing.T) { + assert.Implements(t, (*perun.ChAPI)(nil), new(session.Channel)) +} + +func Test_Getters(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + assert.Equal(t, ch.ID(), fmt.Sprintf("%x", pch.ID())) + assert.Equal(t, ch.Currency(), validOpeningBalInfo.Currency) + assert.Equal(t, ch.Parts(), validOpeningBalInfo.Parts) + assert.Equal(t, ch.ChallengeDurSecs(), uint64(10)) +} + +func Test_GetChInfo(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + + t.Run("happy", func(t *testing.T) { + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + chInfo := ch.GetChInfo() + assert.Equal(t, chInfo.ChID, fmt.Sprintf("%x", pch.ID())) + assert.Equal(t, chInfo.BalInfo.Parts, validOpeningBalInfo.Parts) + assert.Equal(t, chInfo.BalInfo.Currency, validOpeningBalInfo.Currency) + }) + + t.Run("nil_state", func(t *testing.T) { + pch := &mocks.Channel{} + pch.On("ID").Return([32]byte{0, 1, 2}) + pch.On("State").Return(nil) + watcherSignal := make(chan time.Time) + pch.On("Watch").WaitUntil(watcherSignal).Return(nil) + + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + chInfo := ch.GetChInfo() + assert.Zero(t, chInfo) + }) +} + +func Test_SendChUpdate(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + + t.Run("happy", func(t *testing.T) { + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + pch.On("UpdateBy", mock.Anything, mock.Anything).Return(nil) + gotChInfo, err := ch.SendChUpdate(context.Background(), func(s *pchannel.State) {}) + require.NoError(t, err) + assert.NotZero(t, gotChInfo) + }) + + t.Run("UpdateBy_RejectedByPeer", func(t *testing.T) { + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + pch.On("UpdateBy", mock.Anything, mock.Anything).Return(errors.New("rejected by user")) + _, err := ch.SendChUpdate(context.Background(), func(s *pchannel.State) {}) + require.Error(t, err) + }) + + t.Run("channel_closed", func(t *testing.T) { + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, false) + + _, err := ch.SendChUpdate(context.Background(), func(s *pchannel.State) {}) + require.Error(t, err) + }) +} + +func Test_HandleUpdate(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + updatedBalInfo := validOpeningBalInfo + updatedBalInfo.Bal = []string{"0.5", "2.5"} + pch, _ := newMockPCh(t, validOpeningBalInfo) + + nonFinalState := makeState(t, updatedBalInfo, false) + finalState := makeState(t, updatedBalInfo, true) + + t.Run("happy_nonFinal", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + ch.HandleUpdate(*chUpdate, &mocks.ChUpdateResponder{}) + }) + + t.Run("happy_final", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: finalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + ch.HandleUpdate(*chUpdate, &mocks.ChUpdateResponder{}) + }) + + t.Run("happy_unexpected_chUpdate", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, false) + ch.HandleUpdate(*chUpdate, &mocks.ChUpdateResponder{}) + }) +} + +func Test_SubUnsubChUpdate(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + + dummyNotifier := func(notif perun.ChUpdateNotif) {} + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + // SubTest 1: Sub successfully == + err := ch.SubChUpdates(dummyNotifier) + require.NoError(t, err) + + // SubTest 2: Sub again, should error == + err = ch.SubChUpdates(dummyNotifier) + require.Error(t, err) + + // SubTest 3: UnSub successfully == + err = ch.UnsubChUpdates() + require.NoError(t, err) + + // SubTest 4: UnSub again, should error == + err = ch.UnsubChUpdates() + require.Error(t, err) + + t.Run("Sub_channelClosed", func(t *testing.T) { + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, false) + err = ch.SubChUpdates(dummyNotifier) + require.Error(t, err) + }) + t.Run("Unsub_channelClosed", func(t *testing.T) { + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, false) + err = ch.UnsubChUpdates() + require.Error(t, err) + }) +} + +func Test_HandleUpdate_Sub(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + updatedBalInfo := validOpeningBalInfo + updatedBalInfo.Bal = []string{"0.5", "2.5"} + pch, _ := newMockPCh(t, validOpeningBalInfo) + + nonFinalState := makeState(t, updatedBalInfo, false) + t.Run("happy_HandleSub", func(t *testing.T) { + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch.HandleUpdate(*chUpdate, &mocks.ChUpdateResponder{}) + + notifs := make([]perun.ChUpdateNotif, 0, 2) + notifier := func(notif perun.ChUpdateNotif) { + notifs = append(notifs, notif) + } + err := ch.SubChUpdates(notifier) + require.NoError(t, err) + notifRecieved := func() bool { + return len(notifs) == 1 + } + assert.Eventually(t, notifRecieved, 2*time.Second, 100*time.Millisecond) + }) + t.Run("happy_SubHandle", func(t *testing.T) { + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + notifs := make([]perun.ChUpdateNotif, 0, 2) + notifier := func(notif perun.ChUpdateNotif) { + notifs = append(notifs, notif) + } + err := ch.SubChUpdates(notifier) + require.NoError(t, err) + notifRecieved := func() bool { + return len(notifs) == 1 + } + + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch.HandleUpdate(*chUpdate, &mocks.ChUpdateResponder{}) + assert.Eventually(t, notifRecieved, 2*time.Second, 100*time.Millisecond) + }) +} + +func Test_HandleUpdate_Respond(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + updatedBalInfo := validOpeningBalInfo + updatedBalInfo.Bal = []string{"0.5", "2.5"} + pch, _ := newMockPCh(t, validOpeningBalInfo) + + nonFinalState := makeState(t, updatedBalInfo, false) + finalState := makeState(t, updatedBalInfo, true) + + t.Run("happy_Handle_Respond_Accept", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Accept", mock.Anything).Return(nil) + updateID := fmt.Sprintf("%s_%d", ch.ID(), chUpdate.State.Version) + ch.HandleUpdate(*chUpdate, responder) + + chInfo, err := ch.RespondChUpdate(context.Background(), updateID, true) + require.NoError(t, err) + assert.NotZero(t, chInfo) + }) + + t.Run("happy_Handle_Respond_Reject", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Reject", mock.Anything, mock.Anything).Return(nil) + updateID := fmt.Sprintf("%s_%d", ch.ID(), chUpdate.State.Version) + ch.HandleUpdate(*chUpdate, responder) + + chInfo, err := ch.RespondChUpdate(context.Background(), updateID, false) + require.NoError(t, err) + assert.NotZero(t, chInfo) + }) + + t.Run("Handle_Respond_Accept_Error", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Accept", mock.Anything).Return(assert.AnError) + updateID := fmt.Sprintf("%s_%d", ch.ID(), chUpdate.State.Version) + ch.HandleUpdate(*chUpdate, responder) + + _, err := ch.RespondChUpdate(context.Background(), updateID, true) + require.Error(t, err) + t.Log(err) + }) + + t.Run("Handle_Respond_Reject_Error", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Reject", mock.Anything, mock.Anything).Return(assert.AnError) + updateID := fmt.Sprintf("%s_%d", ch.ID(), chUpdate.State.Version) + ch.HandleUpdate(*chUpdate, responder) + + _, err := ch.RespondChUpdate(context.Background(), updateID, false) + require.Error(t, err) + t.Log(err) + }) + + t.Run("Handle_Respond_Unknown_UpdateID", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Accept", mock.Anything).Return(nil) + updateID := "random-update-id" + ch.HandleUpdate(*chUpdate, responder) + + _, err := ch.RespondChUpdate(context.Background(), updateID, true) + require.Error(t, err) + t.Log(err) + }) + + t.Run("Handle_Respond_Expired", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: nonFinalState, + } + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Accept", mock.Anything).Return(nil) + updateID := fmt.Sprintf("%s_%d", ch.ID(), chUpdate.State.Version) + ch.HandleUpdate(*chUpdate, responder) + + time.Sleep(2 * time.Second) + _, err := ch.RespondChUpdate(context.Background(), updateID, true) + require.Error(t, err) + }) + + t.Run("Handle_Respond_ChannelClosed", func(t *testing.T) { + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, false) + updateID := "any-update-id" // A closed channel returns error irrespective of update id. + + _, err := ch.RespondChUpdate(context.Background(), updateID, true) + require.Error(t, err) + t.Log(err) + }) + + t.Run("happy_Handle_Respond_Accept_Final", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: finalState, + } + pch, watcherSignal := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Accept", mock.Anything).Return(nil) + pch.On("SettleSecondary", mock.Anything).Return(nil).Run(func(_ mock.Arguments) { + watcherSignal <- time.Now() // Return the watcher once channel is settled. + }) + pch.On("Close").Return(nil) + + updateID := fmt.Sprintf("%s_%d", ch.ID(), chUpdate.State.Version) + ch.HandleUpdate(*chUpdate, responder) + + chInfo, err := ch.RespondChUpdate(context.Background(), updateID, true) + require.NoError(t, err) + assert.NotZero(t, chInfo) + }) + + t.Run("Handle_Respond_Accept_SettleSecondaryError", func(t *testing.T) { + chUpdate := &pclient.ChannelUpdate{ + State: finalState, + } + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Accept", mock.Anything).Return(nil) + pch.On("SettleSecondary", mock.Anything).Return(assert.AnError) + + updateID := fmt.Sprintf("%s_%d", ch.ID(), chUpdate.State.Version) + ch.HandleUpdate(*chUpdate, responder) + + _, err := ch.RespondChUpdate(context.Background(), updateID, true) + require.Error(t, err) + t.Log(err) + }) +} + +func Test_Close(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + + t.Run("happy_finalizeNoError_settle", func(t *testing.T) { + pch, watcherSignal := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + var finalizer perun.StateUpdater + pch.On("UpdateBy", mock.Anything, mock.MatchedBy(func(gotFinalizer perun.StateUpdater) bool { + finalizer = gotFinalizer + return true + })).Return(nil) + pch.On("Settle", mock.Anything).Return(nil).Run(func(_ mock.Arguments) { + watcherSignal <- time.Now() // Return the watcher once channel is settled. + }) + pch.On("Close").Return(nil) + + gotChInfo, err := ch.Close(context.Background()) + require.NoError(t, err) + assert.NotZero(t, gotChInfo) + + emptyState := pchannel.State{} + finalizer(&emptyState) + assert.True(t, emptyState.IsFinal) + }) + + t.Run("happy_finalizeError_settle", func(t *testing.T) { + pch, watcherSignal := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + pch.On("UpdateBy", mock.Anything, mock.Anything).Return(assert.AnError) + pch.On("Settle", mock.Anything).Return(nil).Run(func(_ mock.Arguments) { + watcherSignal <- time.Now() // Return the watcher once channel is settled. + }) + pch.On("Close").Return(nil) + + gotChInfo, err := ch.Close(context.Background()) + require.NoError(t, err) + assert.NotZero(t, gotChInfo) + }) + + t.Run("happy_closeError", func(t *testing.T) { + pch, watcherSignal := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + pch.On("UpdateBy", mock.Anything, mock.Anything).Return(nil) + pch.On("Settle", mock.Anything).Return(nil).Run(func(_ mock.Arguments) { + watcherSignal <- time.Now() // Return the watcher once channel is settled. + }) + pch.On("Close").Return(assert.AnError) + + gotChInfo, err := ch.Close(context.Background()) + require.NoError(t, err) + assert.NotZero(t, gotChInfo) + }) + + t.Run("settleError", func(t *testing.T) { + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + + pch.On("UpdateBy", mock.Anything, mock.Anything).Return(nil) + pch.On("Settle", mock.Anything).Return(assert.AnError) + + _, err := ch.Close(context.Background()) + require.Error(t, err) + }) + + t.Run("channel_closed", func(t *testing.T) { + pch, _ := newMockPCh(t, validOpeningBalInfo) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, false) + + _, err := ch.Close(context.Background()) + require.Error(t, err) + t.Log(err) + }) +} + +func Test_HandleWatcherReturned(t *testing.T) { + peers := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peers[0].Alias}, + Bal: []string{"1", "2"}, + } + + t.Run("happy_openCh_dropNotif", func(t *testing.T) { + pch, watcherSignal := newMockPCh(t, validOpeningBalInfo) + pch.On("Close").Return(nil) + _ = session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + watcherSignal <- time.Now() + }) + + t.Run("happy_closedCh_dropNotif", func(t *testing.T) { + pch, watcherSignal := newMockPCh(t, validOpeningBalInfo) + pch.On("Close").Return(nil) + _ = session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, false) + watcherSignal <- time.Now() + }) + + t.Run("happy_openCh_hasSub_WatchNoError", func(t *testing.T) { + pch, watcherSignal := newMockPCh(t, validOpeningBalInfo) + pch.On("Close").Return(nil) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + notifs := make([]perun.ChUpdateNotif, 0, 2) + notifer := func(notif perun.ChUpdateNotif) { + notifs = append(notifs, notif) + } + require.NoError(t, ch.SubChUpdates(notifer)) + watcherSignal <- time.Now() + }) + + t.Run("happy_openCh_hasSub_WatchError", func(t *testing.T) { + pch := &mocks.Channel{} + pch.On("ID").Return([32]byte{0, 1, 2}) + pch.On("State").Return(makeState(t, validOpeningBalInfo, false)) + watcherSignal := make(chan time.Time) + pch.On("Watch").WaitUntil(watcherSignal).Return(assert.AnError) + + pch.On("Close").Return(nil) + ch := session.NewChForTest(pch, currency.ETH, validOpeningBalInfo.Parts, 10, true) + notifs := make([]perun.ChUpdateNotif, 0, 2) + notifer := func(notif perun.ChUpdateNotif) { + notifs = append(notifs, notif) + } + require.NoError(t, ch.SubChUpdates(notifer)) + watcherSignal <- time.Now() + }) +} diff --git a/session/export_test.go b/session/export_test.go index 1e845f78..34b0b5ec 100644 --- a/session/export_test.go +++ b/session/export_test.go @@ -16,9 +16,68 @@ package session -import "github.com/hyperledger-labs/perun-node" +import ( + "time" + + pchannel "perun.network/go-perun/channel" + + "github.com/hyperledger-labs/perun-node" + "github.com/hyperledger-labs/perun-node/log" +) // SetWalletBackend is used to set a test wallet backend during tests. func SetWalletBackend(wb perun.WalletBackend) { walletBackend = wb } + +func NewSessionForTest(cfg Config, isOpen bool, chClient perun.ChClient) (*Session, error) { + user, err := NewUnlockedUser(walletBackend, cfg.User) + if err != nil { + return nil, err + } + + chAsset, err := walletBackend.ParseAddr(cfg.Asset) + if err != nil { + return nil, err + } + + idProvider, err := initIDProvider(cfg.IDProviderType, cfg.IDProviderURL, walletBackend, user.PeerID) + if err != nil { + return nil, err + } + + sessionID := calcSessionID(user.OffChainAddr.Bytes()) + timeoutCfg := timeoutConfig{ + onChainTx: cfg.OnChainTxTimeout, + response: cfg.ResponseTimeout, + } + + return &Session{ + Logger: log.NewLoggerWithField("session-id", sessionID), + id: sessionID, + isOpen: isOpen, + timeoutCfg: timeoutCfg, + user: user, + chAsset: chAsset, + chClient: chClient, + idProvider: idProvider, + chs: make(map[string]*Channel), + chProposalResponders: make(map[string]chProposalResponderEntry), + }, nil +} + +func NewChForTest(pch perun.Channel, currency string, parts []string, challengeDurSecs uint64, isOpen bool) *Channel { + timeoutCfg := timeoutConfig{response: 1 * time.Second} + ch := newCh(pch, currency, parts, timeoutCfg, challengeDurSecs) + if isOpen { + ch.status = open + } else { + ch.status = closed + } + ch.Logger = log.NewLoggerWithField("channel-id", ch.id) + return ch +} + +func MakeAllocation(openingBalInfo perun.BalInfo, chAsset pchannel.Asset) (*pchannel.Allocation, error) { + return makeAllocation(openingBalInfo, chAsset) +} diff --git a/session/session.go b/session/session.go index 463c2357..4cb5fe4f 100644 --- a/session/session.go +++ b/session/session.go @@ -52,7 +52,8 @@ func init() { } type ( - session struct { + // Session implements perun.SessionAPI. + Session struct { log.Logger psync.Mutex @@ -64,7 +65,7 @@ type ( chClient perun.ChClient idProvider perun.IDProvider - chs map[string]*channel + chs map[string]*Channel chProposalNotifier perun.ChProposalNotifier chProposalNotifsCache []perun.ChProposalNotif @@ -74,21 +75,33 @@ type ( chProposalResponderEntry struct { proposal pclient.ChannelProposal notif perun.ChProposalNotif - responder chProposalResponder + responder ChProposalResponder } - //go:generate mockery --name chProposalResponder --output ../internal/mocks - - // Proposal Responder defines the methods on proposal responder that will be used by the perun node. - chProposalResponder interface { - Accept(context.Context, *pclient.ChannelProposalAcc) (*pclient.Channel, error) + // ChProposalResponder defines the methods on proposal responder that will be used by the perun node. + ChProposalResponder interface { + Accept(context.Context, *pclient.ChannelProposalAcc) (perun.Channel, error) Reject(ctx context.Context, reason string) error } ) +//go:generate mockery --name ChProposalResponder --output ../internal/mocks + +// chProposalResponderWrapped is a wrapper around pclient.ProposalResponder that returns a channel of +// interface type instead of struct type. This enables easier mocking of the returned value in tests. +type chProposalResponderWrapped struct { + *pclient.ProposalResponder +} + +// Accept is a wrapper around the original function, that returns a channel of interface type instead of struct type. +func (r *chProposalResponderWrapped) Accept(ctx context.Context, proposalAcc *pclient.ChannelProposalAcc) ( + perun.Channel, error) { + return r.ProposalResponder.Accept(ctx, proposalAcc) +} + // New initializes a SessionAPI instance for the given configuration and returns an // instance of it. All methods on it are safe for concurrent use. -func New(cfg Config) (*session, error) { +func New(cfg Config) (*Session, error) { user, err := NewUnlockedUser(walletBackend, cfg.User) if err != nil { return nil, err @@ -130,7 +143,7 @@ func New(cfg Config) (*session, error) { onChainTx: cfg.OnChainTxTimeout, response: cfg.ResponseTimeout, } - sess := &session{ + sess := &Session{ Logger: log.NewLoggerWithField("session-id", sessionID), id: sessionID, isOpen: true, @@ -139,7 +152,7 @@ func New(cfg Config) (*session, error) { chAsset: chAsset, chClient: chClient, idProvider: idProvider, - chs: make(map[string]*channel), + chs: make(map[string]*Channel), chProposalResponders: make(map[string]chProposalResponderEntry), } err = sess.chClient.RestoreChs(sess.handleRestoredCh) @@ -180,11 +193,12 @@ func calcSessionID(userOffChainAddr []byte) string { return fmt.Sprintf("%x", h.Sum(nil)) } -func (s *session) ID() string { +// ID implements sessionAPI.ID. +func (s *Session) ID() string { return s.id } -func (s *session) handleRestoredCh(pch *pclient.Channel) { +func (s *Session) handleRestoredCh(pch perun.Channel) { s.Debugf("found channel in persistence: 0x%x", pch.ID()) // Restore only those channels that are in acting phase. @@ -211,7 +225,8 @@ func (s *session) handleRestoredCh(pch *pclient.Channel) { s.Debugf("restored channel from persistence: %v", ch.getChInfo()) } -func (s *session) AddPeerID(peerID perun.PeerID) error { +// AddPeerID implements sessionAPI.AddPeerID. +func (s *Session) AddPeerID(peerID perun.PeerID) error { s.Debugf("Received request: session.AddPeerID. Params %+v", peerID) s.Lock() defer s.Unlock() @@ -227,7 +242,8 @@ func (s *session) AddPeerID(peerID perun.PeerID) error { return perun.GetAPIError(err) } -func (s *session) GetPeerID(alias string) (perun.PeerID, error) { +// GetPeerID implements sessionAPI.GetPeerID. +func (s *Session) GetPeerID(alias string) (perun.PeerID, error) { s.Debugf("Received request: session.GetPeerID. Params %+v", alias) s.Lock() defer s.Unlock() @@ -244,7 +260,8 @@ func (s *session) GetPeerID(alias string) (perun.PeerID, error) { return peerID, nil } -func (s *session) OpenCh(pctx context.Context, openingBalInfo perun.BalInfo, app perun.App, challengeDurSecs uint64) ( +// OpenCh implements sessionAPI.OpenCh. +func (s *Session) OpenCh(pctx context.Context, openingBalInfo perun.BalInfo, app perun.App, challengeDurSecs uint64) ( perun.ChInfo, error) { s.Debugf("\nReceived request:session.OpenCh Params %+v,%+v,%+v", openingBalInfo, app, challengeDurSecs) // Session is locked only when adding the channel to session. @@ -256,7 +273,7 @@ func (s *session) OpenCh(pctx context.Context, openingBalInfo perun.BalInfo, app sanitizeBalInfo(openingBalInfo) parts, err := retrievePartIDs(openingBalInfo.Parts, s.idProvider) if err != nil { - s.Error(err, "retrieving channel participant IDs using session idProvider") + s.Error(err, "retrieving channel participant IDs using session ID Provider") return perun.ChInfo{}, perun.GetAPIError(err) } registerParts(parts, s.chClient) @@ -393,14 +410,21 @@ func makeAllocation(balInfo perun.BalInfo, chAsset pchannel.Asset) (*pchannel.Al } // addCh adds the channel to session. It locks the session mutex during the operation. -func (s *session) addCh(ch *channel) { +func (s *Session) addCh(ch *Channel) { ch.Logger = log.NewDerivedLoggerWithField(s.Logger, "channel-id", ch.id) s.Lock() s.chs[ch.id] = ch s.Unlock() } -func (s *session) HandleProposal(chProposal pclient.ChannelProposal, responder *pclient.ProposalResponder) { +// HandleProposal is a handler to be registered on the channel client for processing incoming channel proposals. +func (s *Session) HandleProposal(chProposal pclient.ChannelProposal, responder *pclient.ProposalResponder) { + s.HandleProposalWInterface(chProposal, &chProposalResponderWrapped{responder}) +} + +// HandleProposalWInterface is the actual implemention of HandleProposal that takes arguments as interface types. +// It is implemented this way to enable easier testing. +func (s *Session) HandleProposalWInterface(chProposal pclient.ChannelProposal, responder ChProposalResponder) { s.Debugf("SDK Callback: HandleProposal. Params: %+v", chProposal) expiry := time.Now().UTC().Add(s.timeoutCfg.response).Unix() @@ -442,10 +466,10 @@ func (s *session) HandleProposal(chProposal pclient.ChannelProposal, responder * // TODO: (mano) Provide an option for user to configure when more currency interpretters are supported. if s.chProposalNotifier == nil { s.chProposalNotifsCache = append(s.chProposalNotifsCache, notif) - s.Debugf("HandleProposal: Notification cached", notif) + s.Debug("HandleProposal: Notification cached", notif) } else { go s.chProposalNotifier(notif) - s.Debugf("HandleProposal: Notification sent", notif) + s.Debug("HandleProposal: Notification sent", notif) } } @@ -460,7 +484,8 @@ func chProposalNotif(parts []string, curr string, chProposal *pclient.BaseChanne } } -func (s *session) SubChProposals(notifier perun.ChProposalNotifier) error { +// SubChProposals implements sessionAPI.SubChProposals. +func (s *Session) SubChProposals(notifier perun.ChProposalNotifier) error { s.Debug("Received request: session.SubChProposals") s.Lock() defer s.Unlock() @@ -482,7 +507,8 @@ func (s *session) SubChProposals(notifier perun.ChProposalNotifier) error { return nil } -func (s *session) UnsubChProposals() error { +// UnsubChProposals implements sessionAPI.UnsubChProposals. +func (s *Session) UnsubChProposals() error { s.Debug("Received request: session.UnsubChProposals") s.Lock() defer s.Unlock() @@ -498,7 +524,8 @@ func (s *session) UnsubChProposals() error { return nil } -func (s *session) RespondChProposal(pctx context.Context, chProposalID string, accept bool) (perun.ChInfo, error) { +// RespondChProposal implements sessionAPI.RespondChProposal. +func (s *Session) RespondChProposal(pctx context.Context, chProposalID string, accept bool) (perun.ChInfo, error) { s.Debugf("Received request: session.RespondChProposal. Params: %+v, %+v", chProposalID, accept) if !s.isOpen { @@ -534,7 +561,7 @@ func (s *session) RespondChProposal(pctx context.Context, chProposalID string, a return openedChInfo, perun.GetAPIError(err) } -func (s *session) acceptChProposal(pctx context.Context, entry chProposalResponderEntry) (perun.ChInfo, error) { +func (s *Session) acceptChProposal(pctx context.Context, entry chProposalResponderEntry) (perun.ChInfo, error) { ctx, cancel := context.WithTimeout(pctx, s.timeoutCfg.respChProposalAccept(entry.notif.ChallengeDurSecs)) defer cancel() @@ -553,7 +580,7 @@ func (s *session) acceptChProposal(pctx context.Context, entry chProposalRespond return ch.getChInfo(), nil } -func (s *session) rejectChProposal(pctx context.Context, responder chProposalResponder, reason string) error { +func (s *Session) rejectChProposal(pctx context.Context, responder ChProposalResponder, reason string) error { ctx, cancel := context.WithTimeout(pctx, s.timeoutCfg.respChProposalReject()) defer cancel() err := responder.Reject(ctx, reason) @@ -563,7 +590,8 @@ func (s *session) rejectChProposal(pctx context.Context, responder chProposalRes return err } -func (s *session) GetChsInfo() []perun.ChInfo { +// GetChsInfo implements sessionAPI.GetChsInfo. +func (s *Session) GetChsInfo() []perun.ChInfo { s.Debug("Received request: session.GetChInfos") s.Lock() defer s.Unlock() @@ -577,7 +605,8 @@ func (s *session) GetChsInfo() []perun.ChInfo { return openChsInfo } -func (s *session) GetCh(chID string) (perun.ChAPI, error) { +// GetCh implements sessionAPI.GetCh. +func (s *Session) GetCh(chID string) (perun.ChAPI, error) { s.Debugf("Internal call to get channel instance. Params: %+v", chID) s.Lock() @@ -591,7 +620,16 @@ func (s *session) GetCh(chID string) (perun.ChAPI, error) { return ch, nil } -func (s *session) HandleUpdate(chUpdate pclient.ChannelUpdate, responder *pclient.UpdateResponder) { +// HandleUpdate is a handler to be registered on the channel client for processing incoming channel updates. +// This function just identifies the channel to which update is received and invokes the handler for that +// channel. +func (s *Session) HandleUpdate(chUpdate pclient.ChannelUpdate, responder *pclient.UpdateResponder) { + s.HandleUpdateWInterface(chUpdate, responder) +} + +// HandleUpdateWInterface is the actual implemention of HandleUpdate that takes arguments as interface types. +// It is implemented this way to enable easier testing. +func (s *Session) HandleUpdateWInterface(chUpdate pclient.ChannelUpdate, responder ChUpdateResponder) { s.Debugf("SDK Callback: HandleUpdate. Params: %+v", chUpdate) s.Lock() defer s.Unlock() @@ -613,7 +651,8 @@ func (s *session) HandleUpdate(chUpdate pclient.ChannelUpdate, responder *pclien go ch.HandleUpdate(chUpdate, responder) } -func (s *session) Close(force bool) ([]perun.ChInfo, error) { +// Close implements sessionAPI.Close. +func (s *Session) Close(force bool) ([]perun.ChInfo, error) { s.Debug("Received request: session.Close") s.Lock() defer s.Unlock() @@ -664,13 +703,13 @@ func (s *session) Close(force bool) ([]perun.ChInfo, error) { return openChsInfo, s.close() } -func (s *session) unlockAllChs() { +func (s *Session) unlockAllChs() { for _, ch := range s.chs { ch.Unlock() } } -func (s *session) close() error { +func (s *Session) close() error { s.user.OnChain.Wallet.LockAll() s.user.OffChain.Wallet.LockAll() return errors.WithMessage(s.chClient.Close(), "closing session") diff --git a/session/session_integ_test.go b/session/session_integ_test.go index 80a763eb..09e763c5 100644 --- a/session/session_integ_test.go +++ b/session/session_integ_test.go @@ -19,14 +19,12 @@ package session_test import ( - "fmt" "io/ioutil" "math/rand" "os" "testing" copyutil "github.com/otiai10/copy" - "github.com/phayes/freeport" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -37,15 +35,9 @@ import ( "github.com/hyperledger-labs/perun-node/session/sessiontest" ) -func init() { - session.SetWalletBackend(ethereumtest.NewTestWalletBackend()) -} - func Test_Integ_New(t *testing.T) { + peerIDs := newPeerIDs(t, uint(2)) prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) - peerIDs := newPeerIDs(t, prng, uint(2)) - - prng = rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) cfg := sessiontest.NewConfigT(t, prng, peerIDs...) t.Run("happy", func(t *testing.T) { @@ -144,38 +136,44 @@ func Test_Integ_New(t *testing.T) { } func Test_Integ_Persistence(t *testing.T) { - prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) + t.Run("happy", func(t *testing.T) { + prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) + aliceCfg := sessiontest.NewConfigT(t, prng) + // Use idprovider and databaseDir from a session that was persisted already. + // Copy database directory to tmp before using as it will be modifed when reading as well. + // ID provider file can be used as such. + aliceCfg.DatabaseDir = copyDirToTmp(t, "../testdata/session/persistence/alice-database") + aliceCfg.IDProviderURL = "../testdata/session/persistence/alice-idprovider.yaml" - aliceCfg := sessiontest.NewConfigT(t, prng) - // Use idprovider and databaseDir from a session that was persisted already. - // Copy database directory to tmp before using as it will be modifed when reading as well. - // Contacts file can be used as such. - aliceCfg.DatabaseDir = copyDirToTmp(t, "../testdata/session/persistence/alice-database") - aliceCfg.IDProviderURL = "../testdata/session/persistence/alice-idprovider.yaml" + alice, err := session.New(aliceCfg) + require.NoErrorf(t, err, "initializing alice session") + t.Logf("alice session id: %s\n", alice.ID()) + t.Logf("alice database dir is: %s\n", aliceCfg.DatabaseDir) - alice, err := session.New(aliceCfg) - require.NoErrorf(t, err, "initializing alice session") - t.Logf("alice session id: %s\n", alice.ID()) - t.Logf("alice database dir is: %s\n", aliceCfg.DatabaseDir) + require.Equal(t, 2, len(alice.GetChsInfo())) + }) - t.Run("GetChannelInfos", func(t *testing.T) { - t.Run("happy", func(t *testing.T) { - require.Equal(t, 3, len(alice.GetChsInfo())) - }) + t.Run("happy_drop_unknownPeers", func(t *testing.T) { + prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) + aliceCfg := sessiontest.NewConfigT(t, prng) // Get a session config with no peerIDs in the ID provider. + aliceCfg.DatabaseDir = copyDirToTmp(t, "../testdata/session/persistence/alice-database") + + _, err := session.New(aliceCfg) + require.NoErrorf(t, err, "initializing alice session") }) -} -func newPeerIDs(t *testing.T, prng *rand.Rand, n uint) []perun.PeerID { - peerIDs := make([]perun.PeerID, n) - for i := range peerIDs { - port, err := freeport.GetFreePort() + t.Run("err_database_init", func(t *testing.T) { + prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) + aliceCfg := sessiontest.NewConfigT(t, prng) // Get a session config with no peerIDs in the ID provider. + tempFile, err := ioutil.TempFile("", "") require.NoError(t, err) - peerIDs[i].Alias = fmt.Sprintf("%d", i) - peerIDs[i].OffChainAddrString = ethereumtest.NewRandomAddress(prng).String() - peerIDs[i].CommType = "tcp" - peerIDs[i].CommAddr = fmt.Sprintf("127.0.0.1:%d", port) - } - return peerIDs + tempFile.Close() // nolint:errcheck + aliceCfg.DatabaseDir = tempFile.Name() + + _, err = session.New(aliceCfg) + require.Errorf(t, err, "initializing alice session") + t.Log(err) + }) } func newCorruptedYAMLFile(t *testing.T) string { diff --git a/session/session_internal_test.go b/session/session_internal_test.go deleted file mode 100644 index 12df16ac..00000000 --- a/session/session_internal_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2020 - for information on the respective copyright owner -// see the NOTICE file and/or the repository at -// https://github.com/hyperledger-labs/perun-node -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package session - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/hyperledger-labs/perun-node" -) - -func Test_SessionAPI_Interface(t *testing.T) { - assert.Implements(t, (*perun.SessionAPI)(nil), new(session)) -} diff --git a/session/session_test.go b/session/session_test.go new file mode 100644 index 00000000..0dd1c012 --- /dev/null +++ b/session/session_test.go @@ -0,0 +1,694 @@ +// Copyright (c) 2020 - for information on the respective copyright owner +// see the NOTICE file and/or the repository at +// https://github.com/hyperledger-labs/perun-node +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package session_test + +import ( + "context" + "fmt" + "math/rand" + "testing" + "time" + + "github.com/phayes/freeport" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + pchannel "perun.network/go-perun/channel" + pclient "perun.network/go-perun/client" + pwire "perun.network/go-perun/wire" + + "github.com/hyperledger-labs/perun-node" + "github.com/hyperledger-labs/perun-node/blockchain/ethereum/ethereumtest" + "github.com/hyperledger-labs/perun-node/currency" + "github.com/hyperledger-labs/perun-node/idprovider/local" + "github.com/hyperledger-labs/perun-node/internal/mocks" + "github.com/hyperledger-labs/perun-node/session" + "github.com/hyperledger-labs/perun-node/session/sessiontest" +) + +var RandSeedForNewPeerIDs int64 = 121 + +func init() { + session.SetWalletBackend(ethereumtest.NewTestWalletBackend()) +} + +func Test_SessionAPI_Interface(t *testing.T) { + assert.Implements(t, (*perun.SessionAPI)(nil), new(session.Session)) +} + +func newSessionWMockChClient(t *testing.T, isOpen bool, peerIDs ...perun.PeerID) (*session.Session, *mocks.ChClient) { + prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) + cfg := sessiontest.NewConfigT(t, prng, peerIDs...) + chClient := &mocks.ChClient{} + s, err := session.NewSessionForTest(cfg, isOpen, chClient) + require.NoError(t, err) + require.NotNil(t, s) + return s, chClient +} + +func Test_Session_AddPeerID(t *testing.T) { + peerIDs := newPeerIDs(t, uint(2)) + // In openSession, peer0 is already present, peer1 can be added. + openSession, _ := newSessionWMockChClient(t, true, peerIDs[0]) + closedSession, _ := newSessionWMockChClient(t, false, peerIDs[0]) + + t.Run("happy_add_peerID", func(t *testing.T) { + err := openSession.AddPeerID(peerIDs[1]) + require.NoError(t, err) + }) + + t.Run("alias_used_for_diff_peerID", func(t *testing.T) { + peer1WithAlias0 := peerIDs[1] + peer1WithAlias0.Alias = peerIDs[0].Alias + err := openSession.AddPeerID(peer1WithAlias0) + require.Error(t, err) + t.Log(err) + }) + + t.Run("peerID_already_registered", func(t *testing.T) { + err := openSession.AddPeerID(peerIDs[0]) + require.Error(t, err) + t.Log(err) + }) + + t.Run("session_closed", func(t *testing.T) { + err := closedSession.AddPeerID(peerIDs[0]) + require.Error(t, err) + t.Log(err) + }) +} + +func Test_Session_GetPeerID(t *testing.T) { + peerIDs := newPeerIDs(t, uint(1)) + // In openSession, peer0 is present and peer1 is not present. + openSession, _ := newSessionWMockChClient(t, true, peerIDs[0]) + closedSession, _ := newSessionWMockChClient(t, false, peerIDs[0]) + + t.Run("happy_get_contact", func(t *testing.T) { + peerID, err := openSession.GetPeerID(peerIDs[0].Alias) + require.NoError(t, err) + assert.True(t, local.PeerIDEqual(peerID, peerIDs[0])) + }) + + t.Run("contact_not_found", func(t *testing.T) { + _, err := openSession.GetPeerID("unknown-alias") + require.Error(t, err) + t.Log(err) + }) + + t.Run("session_closed", func(t *testing.T) { + _, err := closedSession.GetPeerID(peerIDs[0].Alias) + require.Error(t, err) + t.Log(err) + }) +} + +func makeState(t *testing.T, balInfo perun.BalInfo, isFinal bool) *pchannel.State { + allocation, err := session.MakeAllocation(balInfo, nil) + require.NoError(t, err) + return &pchannel.State{ + ID: [32]byte{0}, + Version: 0, + App: pchannel.NoApp(), + Allocation: *allocation, + Data: pchannel.NoData(), + IsFinal: isFinal, + } +} + +func newMockPCh(t *testing.T, openingBalInfo perun.BalInfo) ( + *mocks.Channel, chan time.Time) { + ch := &mocks.Channel{} + ch.On("ID").Return([32]byte{0, 1, 2}) + ch.On("State").Return(makeState(t, openingBalInfo, false)) + watcherSignal := make(chan time.Time) + ch.On("Watch").WaitUntil(watcherSignal).Return(nil) + return ch, watcherSignal +} + +func Test_Session_OpenCh(t *testing.T) { + peerIDs := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peerIDs[0].Alias}, + Bal: []string{"1", "2"}, + } + app := perun.App{ + Def: pchannel.NoApp(), + Data: pchannel.NoData(), + } + + t.Run("happy_1_own_alias_first", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + session, chClient := newSessionWMockChClient(t, true, peerIDs...) + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, nil) + chClient.On("Register", mock.Anything, mock.Anything).Return() + + chInfo, err := session.OpenCh(context.Background(), validOpeningBalInfo, app, 10) + require.NoError(t, err) + require.NotZero(t, chInfo) + }) + + t.Run("happy_2_own_alias_not_first", func(t *testing.T) { + validOpeningBalInfo2 := validOpeningBalInfo + validOpeningBalInfo2.Parts = []string{peerIDs[0].Alias, perun.OwnAlias} + + ch, _ := newMockPCh(t, validOpeningBalInfo2) + session, chClient := newSessionWMockChClient(t, true, peerIDs...) + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, nil) + chClient.On("Register", mock.Anything, mock.Anything).Return() + + chInfo, err := session.OpenCh(context.Background(), validOpeningBalInfo2, app, 10) + require.NoError(t, err) + require.NotZero(t, chInfo) + }) + + t.Run("session_closed", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + session, chClient := newSessionWMockChClient(t, false, peerIDs...) + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, nil) + chClient.On("Register", mock.Anything, mock.Anything).Return() + + _, err := session.OpenCh(context.Background(), validOpeningBalInfo, app, 10) + require.Error(t, err) + t.Log(err) + }) + + t.Run("missing_parts", func(t *testing.T) { + invalidOpeningBalInfo := validOpeningBalInfo + invalidOpeningBalInfo.Parts = []string{perun.OwnAlias, "missing-part"} + session, _ := newSessionWMockChClient(t, true, peerIDs...) + + _, err := session.OpenCh(context.Background(), invalidOpeningBalInfo, app, 10) + require.Error(t, err) + t.Log(err) + }) + + t.Run("repeated_parts", func(t *testing.T) { + invalidOpeningBalInfo := validOpeningBalInfo + invalidOpeningBalInfo.Parts = []string{peerIDs[0].Alias, peerIDs[0].Alias} + session, _ := newSessionWMockChClient(t, true, peerIDs...) + + _, err := session.OpenCh(context.Background(), invalidOpeningBalInfo, app, 10) + require.Error(t, err) + t.Log(err) + }) + + t.Run("missing_own_alias", func(t *testing.T) { + invalidOpeningBalInfo := validOpeningBalInfo + invalidOpeningBalInfo.Parts = []string{peerIDs[0].Alias, peerIDs[1].Alias} + session, _ := newSessionWMockChClient(t, true, peerIDs...) + + _, err := session.OpenCh(context.Background(), invalidOpeningBalInfo, app, 10) + require.Error(t, err) + t.Log(err) + }) + + t.Run("unsupported_currency", func(t *testing.T) { + invalidOpeningBalInfo := validOpeningBalInfo + invalidOpeningBalInfo.Currency = "unsupported-currency" + session, chClient := newSessionWMockChClient(t, true, peerIDs...) + chClient.On("Register", mock.Anything, mock.Anything).Return() + + _, err := session.OpenCh(context.Background(), invalidOpeningBalInfo, app, 10) + require.Error(t, err) + t.Log(err) + }) + + t.Run("invalid_amount", func(t *testing.T) { + invalidOpeningBalInfo := validOpeningBalInfo + invalidOpeningBalInfo.Bal = []string{"abc", "gef"} + session, chClient := newSessionWMockChClient(t, true, peerIDs...) + chClient.On("Register", mock.Anything, mock.Anything).Return() + + _, err := session.OpenCh(context.Background(), invalidOpeningBalInfo, app, 10) + require.Error(t, err) + t.Log(err) + }) + + t.Run("chClient_proposeChannel_AnError", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + session, chClient := newSessionWMockChClient(t, true, peerIDs...) + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, assert.AnError) + chClient.On("Register", mock.Anything, mock.Anything).Return() + + _, err := session.OpenCh(context.Background(), validOpeningBalInfo, app, 10) + require.Error(t, err) + t.Log(err) + }) + + t.Run("chClient_proposeChannel_PeerRejected", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + session, chClient := newSessionWMockChClient(t, true, peerIDs...) + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, errors.New("channel proposal rejected")) + chClient.On("Register", mock.Anything, mock.Anything).Return() + + _, err := session.OpenCh(context.Background(), validOpeningBalInfo, app, 10) + require.Error(t, err) + t.Log(err) + }) +} + +func newChProposal(t *testing.T, ownAddr, peer perun.PeerID) pclient.ChannelProposal { + prng := rand.New(rand.NewSource(121)) + chAsset := ethereumtest.NewRandomAddress(prng) + + openingBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{peer.Alias, perun.OwnAlias}, + Bal: []string{"1", "2"}, + } + allocation, err := session.MakeAllocation(openingBalInfo, chAsset) + require.NoError(t, err) + + return pclient.NewLedgerChannelProposal(10, ownAddr.OffChainAddr, allocation, + []pwire.Address{peer.OffChainAddr, ownAddr.OffChainAddr}, + pclient.WithApp(pchannel.NoApp(), pchannel.NoData()), pclient.WithRandomNonce()) +} + +func newSessionWChProposal(t *testing.T, peerIDs []perun.PeerID) ( + *session.Session, pclient.ChannelProposal, string) { + session, _ := newSessionWMockChClient(t, true, peerIDs...) + ownPeerID, err := session.GetPeerID(perun.OwnAlias) + require.NoError(t, err) + chProposal := newChProposal(t, ownPeerID, peerIDs[0]) + chProposalID := fmt.Sprintf("%x", chProposal.Proposal().ProposalID()) + return session, chProposal, chProposalID +} + +func Test_Session_HandleProposalWInterface(t *testing.T) { + peerIDs := newPeerIDs(t, uint(1)) + + t.Run("happy", func(t *testing.T) { + session, chProposal, _ := newSessionWChProposal(t, peerIDs) + + responder := &mocks.ChProposalResponder{} + session.HandleProposalWInterface(chProposal, responder) + }) + + t.Run("unknown_peer", func(t *testing.T) { + session, _ := newSessionWMockChClient(t, true) // Don't register any peer in ID provider. + ownPeerID, err := session.GetPeerID(perun.OwnAlias) + require.NoError(t, err) + unknownPeerID := peerIDs[0] + chProposal := newChProposal(t, ownPeerID, unknownPeerID) + + responder := &mocks.ChProposalResponder{} + responder.On("Reject", mock.Anything, mock.Anything).Return(nil) + session.HandleProposalWInterface(chProposal, responder) + }) + + t.Run("session_closed", func(t *testing.T) { + session, _ := newSessionWMockChClient(t, false, peerIDs...) + chProposal := &mocks.ChannelProposal{} + + responder := &mocks.ChProposalResponder{} + session.HandleProposalWInterface(chProposal, responder) + }) +} + +func Test_SubUnsubChProposal(t *testing.T) { + dummyNotifier := func(notif perun.ChProposalNotif) {} + openSession, _ := newSessionWMockChClient(t, true) + closedSession, _ := newSessionWMockChClient(t, false) + + // Note: All sub tests are written at the same level because each sub test modifies the state of session + // and the order of execution needs to be maintained. + + // == SubTest 1: Sub successfully == + err := openSession.SubChProposals(dummyNotifier) + require.NoError(t, err) + + // == SubTest 2: Sub again, should error == + err = openSession.SubChProposals(dummyNotifier) + require.Error(t, err) + + // == SubTest 3: Unsub successfully == + err = openSession.UnsubChProposals() + require.NoError(t, err) + + // == SubTest 4: Unsub again, should error == + err = openSession.UnsubChProposals() + require.Error(t, err) + + t.Run("Sub_sessionClosed", func(t *testing.T) { + err = closedSession.SubChProposals(dummyNotifier) + require.Error(t, err) + }) + + t.Run("Unsub_sessionClosed", func(t *testing.T) { + err = closedSession.UnsubChProposals() + require.Error(t, err) + }) +} + +func Test_HandleProposalWInterface_Sub(t *testing.T) { + peerIDs := newPeerIDs(t, uint(1)) // Aliases of peerIDs are their respective indices in the array. + + t.Run("happy_HandleSub", func(t *testing.T) { + session, chProposal, _ := newSessionWChProposal(t, peerIDs) + + responder := &mocks.ChProposalResponder{} + session.HandleProposalWInterface(chProposal, responder) + notifs := make([]perun.ChProposalNotif, 0, 2) + notifier := func(notif perun.ChProposalNotif) { + notifs = append(notifs, notif) + } + + err := session.SubChProposals(notifier) + require.NoError(t, err) + notifRecieved := func() bool { + return len(notifs) == 1 + } + assert.Eventually(t, notifRecieved, 2*time.Second, 100*time.Millisecond) + }) + + t.Run("happy_SubHandle", func(t *testing.T) { + session, chProposal, _ := newSessionWChProposal(t, peerIDs) + + notifs := make([]perun.ChProposalNotif, 0, 2) + notifier := func(notif perun.ChProposalNotif) { + notifs = append(notifs, notif) + } + err := session.SubChProposals(notifier) + require.NoError(t, err) + responder := &mocks.ChProposalResponder{} + + session.HandleProposalWInterface(chProposal, responder) + notifRecieved := func() bool { + return len(notifs) == 1 + } + assert.Eventually(t, notifRecieved, 2*time.Second, 100*time.Millisecond) + }) +} + +func Test_HandleProposalWInterface_Respond(t *testing.T) { + peerIDs := newPeerIDs(t, uint(1)) // Aliases of peerIDs are their respective indices in the array. + + openingBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{peerIDs[0].Alias, perun.OwnAlias}, + Bal: []string{"1", "2"}, + } + + t.Run("happy_Handle_Respond_Accept", func(t *testing.T) { + session, chProposal, chProposalID := newSessionWChProposal(t, peerIDs) + + ch, _ := newMockPCh(t, openingBalInfo) + responder := &mocks.ChProposalResponder{} + responder.On("Accept", mock.Anything, mock.Anything).Return(ch, nil) + session.HandleProposalWInterface(chProposal, responder) + + gotChInfo, err := session.RespondChProposal(context.Background(), chProposalID, true) + require.NoError(t, err) + assert.Equal(t, gotChInfo.ChID, fmt.Sprintf("%x", ch.ID())) + }) + + t.Run("happy_Handle_Respond_Reject", func(t *testing.T) { + session, chProposal, chProposalID := newSessionWChProposal(t, peerIDs) + + responder := &mocks.ChProposalResponder{} + responder.On("Reject", mock.Anything, mock.Anything).Return(nil) + session.HandleProposalWInterface(chProposal, responder) + + _, err := session.RespondChProposal(context.Background(), chProposalID, false) + assert.NoError(t, err) + }) + + t.Run("happy_Handle_Respond_Accept_Error", func(t *testing.T) { + session, chProposal, chProposalID := newSessionWChProposal(t, peerIDs) + + ch, _ := newMockPCh(t, openingBalInfo) + responder := &mocks.ChProposalResponder{} + responder.On("Accept", mock.Anything, mock.Anything).Return(ch, assert.AnError) + session.HandleProposalWInterface(chProposal, responder) + + _, err := session.RespondChProposal(context.Background(), chProposalID, true) + assert.Error(t, err) + t.Log(err) + }) + + t.Run("Handle_Respond_Reject_Error", func(t *testing.T) { + session, chProposal, chProposalID := newSessionWChProposal(t, peerIDs) + + responder := &mocks.ChProposalResponder{} + responder.On("Reject", mock.Anything, mock.Anything).Return(assert.AnError) + session.HandleProposalWInterface(chProposal, responder) + + _, err := session.RespondChProposal(context.Background(), chProposalID, false) + assert.Error(t, err) + t.Log(err) + }) + + t.Run("Respond_Unknonwn_ProposalID", func(t *testing.T) { + session, _ := newSessionWMockChClient(t, true, peerIDs...) + + _, err := session.RespondChProposal(context.Background(), "unknown-proposal-id", true) + require.Error(t, err) + t.Log(err) + }) + + t.Run("Handle_Respond_Timeout", func(t *testing.T) { + chClient := &mocks.ChClient{} // Dummy ChClient is sufficient as no methods on it will be invoked. + prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) + modifiedCfg := sessiontest.NewConfigT(t, prng, peerIDs...) + modifiedCfg.ResponseTimeout = 1 * time.Second + session, err := session.NewSessionForTest(modifiedCfg, true, chClient) + require.NoError(t, err) + require.NotNil(t, session) + + ownPeerID, err := session.GetPeerID(perun.OwnAlias) + require.NoError(t, err) + chProposal := newChProposal(t, ownPeerID, peerIDs[0]) + chProposalID := fmt.Sprintf("%x", chProposal.Proposal().ProposalID()) + + responder := &mocks.ChProposalResponder{} // Dummy responder is sufficient as no methods on it will be invoked. + session.HandleProposalWInterface(chProposal, responder) + time.Sleep(2 * time.Second) // Wait until the notification expires. + _, err = session.RespondChProposal(context.Background(), chProposalID, true) + assert.Error(t, err) + t.Log(err) + }) + + t.Run("Respond_Session_Closed", func(t *testing.T) { + session, _ := newSessionWMockChClient(t, false, peerIDs...) + + chProposalID := "any-proposal-id" // A closed session returns error irrespective of proposal id. + _, err := session.RespondChProposal(context.Background(), chProposalID, true) + assert.Error(t, err) + t.Log(err) + }) +} + +func Test_ProposeCh_GetChsInfo(t *testing.T) { + peerIDs := newPeerIDs(t, uint(2)) + prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) + cfg := sessiontest.NewConfigT(t, prng, peerIDs...) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peerIDs[0].Alias}, + Bal: []string{"1", "2"}, + } + app := perun.App{ + Def: pchannel.NoApp(), + Data: pchannel.NoData(), + } + ch, _ := newMockPCh(t, validOpeningBalInfo) + chClient := &mocks.ChClient{} + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, nil) + chClient.On("Register", mock.Anything, mock.Anything).Return() + session, err := session.NewSessionForTest(cfg, true, chClient) + require.NoError(t, err) + require.NotNil(t, session) + + chInfo, err := session.OpenCh(context.Background(), validOpeningBalInfo, app, 10) + require.NoError(t, err) + require.NotZero(t, chInfo) + + t.Run("happy", func(t *testing.T) { + chID := fmt.Sprintf("%x", ch.ID()) + chsInfo := session.GetChsInfo() + assert.Len(t, chsInfo, 1) + assert.Equal(t, chsInfo[0].ChID, chID) + }) +} + +func Test_ProposeCh_GetCh(t *testing.T) { + peerIDs := newPeerIDs(t, uint(2)) + prng := rand.New(rand.NewSource(ethereumtest.RandSeedForTestAccs)) + cfg := sessiontest.NewConfigT(t, prng, peerIDs...) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peerIDs[0].Alias}, + Bal: []string{"1", "2"}, + } + app := perun.App{ + Def: pchannel.NoApp(), + Data: pchannel.NoData(), + } + ch, _ := newMockPCh(t, validOpeningBalInfo) + chClient := &mocks.ChClient{} + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, nil) + chClient.On("Register", mock.Anything, mock.Anything).Return() + session, err := session.NewSessionForTest(cfg, true, chClient) + require.NoError(t, err) + require.NotNil(t, session) + + chInfo, err := session.OpenCh(context.Background(), validOpeningBalInfo, app, 10) + require.NoError(t, err) + require.NotZero(t, chInfo) + + t.Run("happy", func(t *testing.T) { + chID := fmt.Sprintf("%x", ch.ID()) + gotCh, err := session.GetCh(chID) + require.NoError(t, err) + assert.Equal(t, gotCh.ID(), chID) + }) + + t.Run("unknownChID", func(t *testing.T) { + _, err := session.GetCh("unknown-ch-ID") + require.Error(t, err) + }) +} + +func newSessionWCh(t *testing.T, peerIDs []perun.PeerID, openingBalInfo perun.BalInfo, + ch perun.Channel) *session.Session { + app := perun.App{ + Def: pchannel.NoApp(), + Data: pchannel.NoData(), + } + session, chClient := newSessionWMockChClient(t, true, peerIDs...) + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, nil) + chClient.On("Register", mock.Anything, mock.Anything).Return() + chClient.On("Close", mock.Anything).Return(nil) + + chInfo, err := session.OpenCh(context.Background(), openingBalInfo, app, 10) + require.NoError(t, err) + require.NotZero(t, chInfo) + + return session +} + +func Test_ProposeCh_CloseSession(t *testing.T) { + peerIDs := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peerIDs[0].Alias}, + Bal: []string{"1", "2"}, + } + t.Run("happy_no_force", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + ch.On("Phase").Return(pchannel.Acting) + session, chClient := newSessionWMockChClient(t, true, peerIDs...) + chClient.On("ProposeChannel", mock.Anything, mock.Anything).Return(ch, nil) + chClient.On("Register", mock.Anything, mock.Anything).Return() + chClient.On("Close", mock.Anything).Return(nil) + + persistedChs, err := session.Close(false) + require.NoError(t, err) + assert.Len(t, persistedChs, 0) + }) + t.Run("happy_force", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + ch.On("Phase").Return(pchannel.Acting) + session := newSessionWCh(t, peerIDs, validOpeningBalInfo, ch) + + persistedChs, err := session.Close(true) + require.NoError(t, err) + assert.Len(t, persistedChs, 1) + }) + t.Run("no_force_openChs", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + ch.On("Phase").Return(pchannel.Acting) + session := newSessionWCh(t, peerIDs, validOpeningBalInfo, ch) + + _, err := session.Close(false) + require.Error(t, err) + t.Log(err) + }) + t.Run("no_force_openChs", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + ch.On("Phase").Return(pchannel.Acting) + session := newSessionWCh(t, peerIDs, validOpeningBalInfo, ch) + + _, err := session.Close(false) + require.Error(t, err) + t.Log(err) + }) + + t.Run("force_unexpectedPhaseChs", func(t *testing.T) { + ch, _ := newMockPCh(t, validOpeningBalInfo) + ch.On("Phase").Return(pchannel.Registering) + session := newSessionWCh(t, peerIDs, validOpeningBalInfo, ch) + + _, err := session.Close(false) + require.Error(t, err) + t.Log(err) + }) + t.Run("session_closed", func(t *testing.T) { + session, _ := newSessionWMockChClient(t, false) + + _, err := session.Close(false) + require.Error(t, err) + t.Log(err) + }) +} + +func Test_Session_HandleUpdateWInterface(t *testing.T) { + t.Run("happy", func(t *testing.T) { + peerIDs := newPeerIDs(t, uint(2)) + validOpeningBalInfo := perun.BalInfo{ + Currency: currency.ETH, + Parts: []string{perun.OwnAlias, peerIDs[0].Alias}, + Bal: []string{"1", "2"}, + } + updatedBalInfo := validOpeningBalInfo + updatedBalInfo.Bal = []string{"0.5", "2.5"} + + chUpdate := &pclient.ChannelUpdate{ + State: makeState(t, updatedBalInfo, false), + } + session, _ := newSessionWMockChClient(t, true) + responder := &mocks.ChUpdateResponder{} + responder.On("Reject", mock.Anything, mock.Anything).Return(nil) + session.HandleUpdateWInterface(*chUpdate, responder) + }) + t.Run("session_closed", func(t *testing.T) { + session, _ := newSessionWMockChClient(t, false) + session.HandleUpdate(pclient.ChannelUpdate{}, new(pclient.UpdateResponder)) + }) +} + +func newPeerIDs(t *testing.T, n uint) []perun.PeerID { + ethereumBackend := ethereumtest.NewTestWalletBackend() + // Use same prng for each call. + prng := rand.New(rand.NewSource(RandSeedForNewPeerIDs)) + peerIDs := make([]perun.PeerID, n) + for i := range peerIDs { + port, err := freeport.GetFreePort() + require.NoError(t, err) + peerIDs[i].Alias = fmt.Sprintf("%d", i) + peerIDs[i].OffChainAddrString = ethereumtest.NewRandomAddress(prng).String() + peerIDs[i].CommType = "tcp" + peerIDs[i].CommAddr = fmt.Sprintf("127.0.0.1:%d", port) + + peerIDs[i].OffChainAddr, err = ethereumBackend.ParseAddr(peerIDs[i].OffChainAddrString) + require.NoError(t, err) + } + return peerIDs +} diff --git a/testdata/session/persistence/alice-database/000001.log b/testdata/session/persistence/alice-database/000001.log index 2f32f33c73251947ad0c4db12f87bb20a37b0411..946e1b3ef1e85ec9b7adb2a727e9bd658931b20c 100644 GIT binary patch literal 6056 zcmbY=bc`2zCOhENI#HlYxEK1BRE+OO)E~wj?7^>?O)q-{& zE{*Y%7nfM*boX&%sq!-MUphzM^cG!N#y#K3t^B!P_WKM51+5+n&XXSxC+NPIaw)9h zz}>%v5^RhzS+_U6-1@O!B>OwDUMfgUEh^R`-e#iB%}6Xx1%{L{aUoS)l9-;Emu>~5 zOHxVKm6>j3K$b2;h5%^~)oBcjobWgRrXQyOV5nGOiwvt|V9Lr%%^{e$V1{6eJ>m`N zSa#U_I0GX$vg61J(J(?u5NZJfBLfqV899k#ZsNJhZRaNh;TpHx7^3>*cRqjoXnl%| zz$Af4piB5DbcumuXaB0wjV&8o=lNbbawvAs*3~a+%@uxX?6`JUF8@;NxzDKr?pa~G z?aoD}U%F*{XHUuIi8rpklS$>yp5n5VJ!HijnFymBLN6E@`JpahB_~WEd4{w??-nB? zZPElY#X&-G?2?u&naD|lq#GHLbPaas`i9$_4s_$CMB;MsO zdULNdb74;HHt`#LuaEV|J^R5U{ORN^egQmdU! zI;uYkwiqrDf5~LiIc4VrFX@9E$~jD$gA%N8Eyrw?_|9r-GH@>BGfXZ`i`IpaNmIIyN)+Z&!;7>;M=-sap3wn;m7>S zX9&mkikv|h9===pVp z`kx(lcU(T`c74W~%Qh-s8oFMbw6s>5_gUtIR^AL?eWwg{2~vGWe3&5DcLa4o8~MQc zj*wFF(gZWbK|*ot5?%Iz%P3fVM<_TDIf{bPhWmnd5U{?}Ku&m&`i`LWkW9o}HC=HZ zu)foTnt)W_c?S7EWe@aM_#7gutS)xu>Gm&t*)_~R&Mq}m_tcpXJrkq8Bd7tbz9Xm} zTHg^;M^1gWV`*~Nouyu%ZFt$3%e-$VsJSlqzfRN|qU;pG`}|BiTF$SIH{U4{&S8D^V-^_?L!{SaH<5weA-`i`I>>*jOk0P8zrWXD13 zJ3?9!xf2wkVT6=JEg-MHBV;(NzEhWf?F6i4Ore$`)prC!km#XQ%M!2V&G`pB71kv;y^}~)Wsc9Y=13U(=Lj+=`vI;7k{BX)7SK) z`uBPAJvkmy>weT^CAeCOu3&f+k|wqEyd~!(ck{d*{4zhM*?R!%J9DT@km@_)!vwj$ zBd7~e-w{$uUYcO0I7ldtU82iAl=_ZPa3FFN1*MJY{1$OweP@H5@F4XaLF=LQ9rGV{ zM_`d)3pD{;UeTmT0A)`QAZ5r0K2%DiZ&;##zexvXM0|d+pvFy;34Y#VFKD;igKdtO>A{{D=3`|*hsW}7_7t9cBu}8ci zNrIJ5#~B#8ksU`)h=vhLf=~+>7#Wy=%*aU`a}&>1ZaY6AAivw|g)T90oU^F3{LQ0Q)^tXviKllc_v~bC-WS5FmbvP0PDwv2<1Q2Kq;LNv zRp0ch9rIti)b-8e*_rdFe+`u75fN|SDX9=Bv!MFIx)+R${7{#$k`pG7JVRQccZ-pc zHfe&H;vk_oc1cT?Oys0Na!Z@(t9JagW?&RSPIx3`B4+NZn)iV9ohZ}jJwZR9M zR^&x~pOl$#@_SIuqdemWV;C5Db`r0u00yN`b_)c@PoDWcJNy-{$Z z!YRA7a^9_XU#r~kb9^lk-XnkdySkaj2AP{{Q`64&X?6bA>|3_(W`oY5t7oMPj@n-Z z)_2NKmmt-5#D@uTeMe9iw2=?2?+7U+FHJC0961z+DQRUCtiB@@9Eco6L22{K_~A=n zeW!t(@F4XaLF*xzh`GDN{4%h<(}bFURNpO?KYZ0ZeW4bMM!DdZu=D%t_GO&ioVeH6 z;2WbX+w7Ow81)@N4QTy4g6g659U*my`VL#^h^^RVP*|1M5+%-NdRFnZuBqIa_DRyx zzB5dgTsU7gAz4&q#%d{OoD;GdUf&UGGtuUv)OQ4(0WZ&x`*+0aLQa7s=`v*48y#2$ ztnUn==?AI4!xkj)s)~>;u=);L>=AEB`UevQV0~wd>^Mk$N63Ce?gWKs7$N0Q3&^YQ z2pJBm@Ah7lbOqKjrcld}>N|oaz|#jXrVSn0+9aN>wCeb}@W|v(&O0)HRhgP7u*EMj zeBrRTA*|?0B~!Fh_lvW!%%yYW_phuq4nFZ-fPu5$wP(`X#+N@3dL^HO**ucKcUl$O#Wp-x0JPTHgTxHB=mg diff --git a/testdata/session/persistence/alice-database/LOG b/testdata/session/persistence/alice-database/LOG index 8389b001..f80f4f2f 100644 --- a/testdata/session/persistence/alice-database/LOG +++ b/testdata/session/persistence/alice-database/LOG @@ -1,8 +1,8 @@ -=============== Nov 10, 2020 (IST) =============== -12:51:14.669129 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed -12:51:14.672178 db@open opening -12:51:14.672595 version@stat F·[] S·0B[] Sc·[] -12:51:14.673837 db@janitor F·2 G·0 -12:51:14.673863 db@open done T·1.671409ms -12:52:23.792321 db@close closing -12:52:23.792410 db@close done T·86.652µs +=============== Dec 18, 2020 (IST) =============== +06:07:10.859512 log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock Ke·KeyError D·DroppedEntry L·Level Q·SeqNum T·TimeElapsed +06:07:10.862644 db@open opening +06:07:10.864140 version@stat F·[] S·0B[] Sc·[] +06:07:10.865180 db@janitor F·2 G·0 +06:07:10.865191 db@open done T·2.532866ms +06:09:06.021655 db@close closing +06:09:06.021732 db@close done T·76.389µs