Skip to content

Commit

Permalink
feat: Reusability of Mock module for middleware integration tests (#432)
Browse files Browse the repository at this point in the history
* proof of concept for mock middleware

* add mock base app implementation and add documentation

* add changelog

* update wording

* Update CHANGELOG.md

* rename BaseApp to IBCApp

* fix testing readme

* Update testing/README.md

Co-authored-by: Aditya <adityasripal@gmail.com>
  • Loading branch information
colin-axner and AdityaSripal authored Sep 27, 2021
1 parent a19d1ec commit 6e56b04
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
### Features

* [\#384](https://github.com/cosmos/ibc-go/pull/384) Added `NegotiateAppVersion` method to `IBCModule` interface supported by a gRPC query service in `05-port`. This provides routing of requests to the desired application module callback, which in turn performs application version negotiation.
* [\#432](https://github.com/cosmos/ibc-go/pull/432) Introduce `MockIBCApp` struct to the mock module. Allows the mock module to be reused to perform custom logic on each IBC App interface function. This might be useful when testing out IBC applications written as middleware.

## [v1.2.0](https://github.com/cosmos/ibc-go/releases/tag/v1.2.0) - 2021-09-10

Expand Down
16 changes: 16 additions & 0 deletions testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,19 @@ func GetTransferSimApp(chain *ibctesting.TestChain) *simapp.SimApp {
return app
}
```

### Middleware Testing

When writing IBC applications acting as middleware, it might be desirable to test integration points.
This can be done by wiring a middleware stack in the app.go file using existing applications as middleware and IBC base applications.
The mock module may also be leveraged to act as a base application in the instance that such an application is not available for testing or causes dependency concerns.

The mock module contains a `MockIBCApp`. This struct contains a function field for every IBC App Module callback.
Each of these functions can be individually set to mock expected behaviour of a base application.

For example, if one wanted to test that the base application cannot affect the outcome of the `OnChanOpenTry` callback, the mock module base application callback could be updated as such:
```go
mockModule.IBCApp.OnChanOpenTry = func(ctx sdk.Context, portID, channelID, version string) error {
return fmt.Errorf("mock base app must not be called for OnChanOpenTry")
}
```
95 changes: 95 additions & 0 deletions testing/mock/ibc_app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package mock

import (
sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"

channeltypes "github.com/cosmos/ibc-go/v2/modules/core/04-channel/types"
"github.com/cosmos/ibc-go/v2/modules/core/exported"
)

type MockIBCApp struct {
OnChanOpenInit func(
ctx sdk.Context,
order channeltypes.Order,
connectionHops []string,
portID string,
channelID string,
channelCap *capabilitytypes.Capability,
counterparty channeltypes.Counterparty,
version string,
) error

OnChanOpenTry func(
ctx sdk.Context,
order channeltypes.Order,
connectionHops []string,
portID,
channelID string,
channelCap *capabilitytypes.Capability,
counterparty channeltypes.Counterparty,
version,
counterpartyVersion string,
) error

OnChanOpenAck func(
ctx sdk.Context,
portID,
channelID string,
counterpartyVersion string,
) error

OnChanOpenConfirm func(
ctx sdk.Context,
portID,
channelID string,
) error

OnChanCloseInit func(
ctx sdk.Context,
portID,
channelID string,
) error

OnChanCloseConfirm func(
ctx sdk.Context,
portID,
channelID string,
) error

// OnRecvPacket must return an acknowledgement that implements the Acknowledgement interface.
// In the case of an asynchronous acknowledgement, nil should be returned.
// If the acknowledgement returned is successful, the state changes on callback are written,
// otherwise the application state changes are discarded. In either case the packet is received
// and the acknowledgement is written (in synchronous cases).
OnRecvPacket func(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) exported.Acknowledgement

OnAcknowledgementPacket func(
ctx sdk.Context,
packet channeltypes.Packet,
acknowledgement []byte,
relayer sdk.AccAddress,
) error

OnTimeoutPacket func(
ctx sdk.Context,
packet channeltypes.Packet,
relayer sdk.AccAddress,
) error

// NegotiateAppVersion performs application version negotiation given the provided channel ordering, connectionID, portID, counterparty and proposed version.
// An error is returned if version negotiation cannot be performed. For example, an application module implementing this interface
// may decide to return an error in the event of the proposed version being incompatible with it's own
NegotiateAppVersion func(
ctx sdk.Context,
order channeltypes.Order,
connectionID string,
portID string,
counterparty channeltypes.Counterparty,
proposedVersion string,
) (version string, err error)
}
62 changes: 52 additions & 10 deletions testing/mock/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type AppModule struct {
AppModuleBasic
scopedKeeper capabilitykeeper.ScopedKeeper
portKeeper PortKeeper
IBCApp MockIBCApp // base application of an IBC middleware stack
}

// NewAppModule returns a mock AppModule instance.
Expand Down Expand Up @@ -153,9 +154,14 @@ func (am AppModule) EndBlock(ctx sdk.Context, req abci.RequestEndBlock) []abci.V

// OnChanOpenInit implements the IBCModule interface.
func (am AppModule) OnChanOpenInit(
ctx sdk.Context, _ channeltypes.Order, _ []string, portID string,
channelID string, chanCap *capabilitytypes.Capability, _ channeltypes.Counterparty, _ string,
ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string,
channelID string, chanCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version string,
) error {
if am.IBCApp.OnChanOpenInit != nil {
return am.IBCApp.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, version)

}

// Claim channel capability passed back by IBC module
if err := am.scopedKeeper.ClaimCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)); err != nil {
return err
Expand All @@ -166,9 +172,13 @@ func (am AppModule) OnChanOpenInit(

// OnChanOpenTry implements the IBCModule interface.
func (am AppModule) OnChanOpenTry(
ctx sdk.Context, _ channeltypes.Order, _ []string, portID string,
channelID string, chanCap *capabilitytypes.Capability, _ channeltypes.Counterparty, _, _ string,
ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string,
channelID string, chanCap *capabilitytypes.Capability, counterparty channeltypes.Counterparty, version, counterpartyVersion string,
) error {
if am.IBCApp.OnChanOpenTry != nil {
return am.IBCApp.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, chanCap, counterparty, version, counterpartyVersion)

}
// Claim channel capability passed back by IBC module
if err := am.scopedKeeper.ClaimCapability(ctx, chanCap, host.ChannelCapabilityPath(portID, channelID)); err != nil {
return err
Expand All @@ -178,27 +188,47 @@ func (am AppModule) OnChanOpenTry(
}

// OnChanOpenAck implements the IBCModule interface.
func (am AppModule) OnChanOpenAck(sdk.Context, string, string, string) error {
func (am AppModule) OnChanOpenAck(ctx sdk.Context, portID string, channelID string, counterpartyVersion string) error {
if am.IBCApp.OnChanOpenAck != nil {
return am.IBCApp.OnChanOpenAck(ctx, portID, channelID, counterpartyVersion)
}

return nil
}

// OnChanOpenConfirm implements the IBCModule interface.
func (am AppModule) OnChanOpenConfirm(sdk.Context, string, string) error {
func (am AppModule) OnChanOpenConfirm(ctx sdk.Context, portID, channelID string) error {
if am.IBCApp.OnChanOpenConfirm != nil {
return am.IBCApp.OnChanOpenConfirm(ctx, portID, channelID)
}

return nil
}

// OnChanCloseInit implements the IBCModule interface.
func (am AppModule) OnChanCloseInit(sdk.Context, string, string) error {
func (am AppModule) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error {
if am.IBCApp.OnChanCloseInit != nil {
return am.IBCApp.OnChanCloseInit(ctx, portID, channelID)
}

return nil
}

// OnChanCloseConfirm implements the IBCModule interface.
func (am AppModule) OnChanCloseConfirm(sdk.Context, string, string) error {
func (am AppModule) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string) error {
if am.IBCApp.OnChanCloseConfirm != nil {
return am.IBCApp.OnChanCloseConfirm(ctx, portID, channelID)
}

return nil
}

// OnRecvPacket implements the IBCModule interface.
func (am AppModule) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) exported.Acknowledgement {
if am.IBCApp.OnRecvPacket != nil {
return am.IBCApp.OnRecvPacket(ctx, packet, relayer)
}

// set state by claiming capability to check if revert happens return
_, err := am.scopedKeeper.NewCapability(ctx, MockRecvCanaryCapabilityName+strconv.Itoa(int(packet.GetSequence())))
if err != nil {
Expand All @@ -216,7 +246,11 @@ func (am AppModule) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, re
}

// OnAcknowledgementPacket implements the IBCModule interface.
func (am AppModule) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, _ []byte, _ sdk.AccAddress) error {
func (am AppModule) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error {
if am.IBCApp.OnAcknowledgementPacket != nil {
return am.IBCApp.OnAcknowledgementPacket(ctx, packet, acknowledgement, relayer)
}

_, err := am.scopedKeeper.NewCapability(ctx, MockAckCanaryCapabilityName+strconv.Itoa(int(packet.GetSequence())))
if err != nil {
// application callback called twice on same packet sequence
Expand All @@ -228,7 +262,11 @@ func (am AppModule) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes
}

// OnTimeoutPacket implements the IBCModule interface.
func (am AppModule) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, _ sdk.AccAddress) error {
func (am AppModule) OnTimeoutPacket(ctx sdk.Context, packet channeltypes.Packet, relayer sdk.AccAddress) error {
if am.IBCApp.OnTimeoutPacket != nil {
return am.IBCApp.OnTimeoutPacket(ctx, packet, relayer)
}

_, err := am.scopedKeeper.NewCapability(ctx, MockTimeoutCanaryCapabilityName+strconv.Itoa(int(packet.GetSequence())))
if err != nil {
// application callback called twice on same packet sequence
Expand All @@ -248,6 +286,10 @@ func (am AppModule) NegotiateAppVersion(
counterparty channeltypes.Counterparty,
proposedVersion string,
) (string, error) {
if am.IBCApp.NegotiateAppVersion != nil {
return am.IBCApp.NegotiateAppVersion(ctx, order, connectionID, portID, counterparty, proposedVersion)
}

if proposedVersion != Version { // allow testing of error scenarios
return "", errors.New("failed to negotiate app version")
}
Expand Down
5 changes: 5 additions & 0 deletions testing/simapp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,11 @@ func (app *SimApp) GetScopedIBCKeeper() capabilitykeeper.ScopedKeeper {
return app.ScopedIBCKeeper
}

// GetMockModule returns the mock module in the testing application
func (app *SimApp) GetMockModule() ibcmock.AppModule {
return app.mm.Modules[ibcmock.ModuleName].(ibcmock.AppModule)
}

// GetTxConfig implements the TestingApp interface.
func (app *SimApp) GetTxConfig() client.TxConfig {
return MakeTestEncodingConfig().TxConfig
Expand Down

0 comments on commit 6e56b04

Please sign in to comment.