diff --git a/CHANGELOG.md b/CHANGELOG.md index f246be20c62..c9c9df3f04a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/testing/README.md b/testing/README.md index 1befdd04ee8..f7a71bb1348 100644 --- a/testing/README.md +++ b/testing/README.md @@ -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") + } +``` diff --git a/testing/mock/ibc_app.go b/testing/mock/ibc_app.go new file mode 100644 index 00000000000..92e36a697ef --- /dev/null +++ b/testing/mock/ibc_app.go @@ -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) +} diff --git a/testing/mock/mock.go b/testing/mock/mock.go index 8967f0aceb2..00dc8e94751 100644 --- a/testing/mock/mock.go +++ b/testing/mock/mock.go @@ -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. @@ -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 @@ -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 @@ -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 { @@ -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 @@ -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 @@ -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") } diff --git a/testing/simapp/app.go b/testing/simapp/app.go index f16cbc112c3..cc2aefc03f6 100644 --- a/testing/simapp/app.go +++ b/testing/simapp/app.go @@ -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