Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Reusability of Mock module for middleware integration tests #432

Merged
merged 10 commits into from
Sep 27, 2021
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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha nice call @damiannolan on not using _ for unused fields, since I ended up using them now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the reason not to use _? In my opinion the _ can be useful because it can already tell the user of the function that the argument is not used just by looking at the function signature, and therefore doesn't need to bother to pass a proper value. Otherwise that information is hidden to the user and they will need to look into the implementation to figure that out. As an alternative, could the unused parameters be listed in the function's documentation?

) (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