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

[Testing, Tooling] Expose integration app via gRPC/HTTP/WS #1017

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
164 changes: 164 additions & 0 deletions testutil/e2e/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package e2e

import (
"context"
"errors"
"net"
"net/http"
"sync"
"testing"

comettypes "github.com/cometbft/cometbft/types"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/gorilla/websocket"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/grpc-ecosystem/grpc-gateway/utilities"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

coretypes "github.com/cometbft/cometbft/rpc/core/types"

"github.com/pokt-network/poktroll/testutil/integration"
"github.com/pokt-network/poktroll/testutil/testclient"
)

// E2EApp wraps an integration.App and provides both gRPC and WebSocket servers for end-to-end testing
type E2EApp struct {
*integration.App
grpcServer *grpc.Server
grpcListener net.Listener
wsServer *http.Server
wsListener net.Listener
wsUpgrader websocket.Upgrader
wsConnMutex sync.RWMutex
wsConnections map[*websocket.Conn]map[string]struct{} // maps connections to their subscribed event queries
resultEventChan chan *coretypes.ResultEvent
}

// NewE2EApp creates a new E2EApp instance with integration.App, gRPC, and WebSocket servers
func NewE2EApp(t *testing.T, opts ...integration.IntegrationAppOptionFn) *E2EApp {
t.Helper()
ctx := context.Background()

// Initialize and start gRPC server
creds := insecure.NewCredentials()
grpcServer := grpc.NewServer(grpc.Creds(creds))
mux := runtime.NewServeMux()

rootPattern, err := runtime.NewPattern(
1,
[]int{int(utilities.OpLitPush), int(utilities.OpNop)},
[]string{""},
"",
)
require.NoError(t, err)

// Create the integration app
opts = append(opts, integration.WithGRPCServer(grpcServer))
app := integration.NewCompleteIntegrationApp(t, opts...)
app.RegisterGRPCServer(grpcServer)

flagSet := testclient.NewFlagSet(t, "tcp://127.0.0.1:42070")
keyRing := keyring.NewInMemory(app.GetCodec())
clientCtx := testclient.NewLocalnetClientCtx(t, flagSet).WithKeyring(keyRing)

// Register the handler with the mux
client, err := grpc.NewClient("127.0.0.1:42069", grpc.WithInsecure())
require.NoError(t, err)

for _, mod := range app.GetModuleManager().Modules {
mod.(module.AppModuleBasic).RegisterGRPCGatewayRoutes(clientCtx, mux)
}

// Create listeners for gRPC, WebSocket, and HTTP
grpcListener, err := net.Listen("tcp", "127.0.0.1:42069")
require.NoError(t, err, "failed to create gRPC listener")

wsListener, err := net.Listen("tcp", "127.0.0.1:6969")
require.NoError(t, err, "failed to create WebSocket listener")

e2eApp := &E2EApp{
App: app,
grpcListener: grpcListener,
grpcServer: grpcServer,
wsListener: wsListener,
wsConnections: make(map[*websocket.Conn]map[string]struct{}),
wsUpgrader: websocket.Upgrader{},
resultEventChan: make(chan *coretypes.ResultEvent),
}

mux.Handle(http.MethodPost, rootPattern, newPostHandler(ctx, client, e2eApp))

go func() {
if err := e2eApp.grpcServer.Serve(grpcListener); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}
}()

// Initialize and start WebSocket server
e2eApp.wsServer = newWebSocketServer(e2eApp)
go func() {
if err := e2eApp.wsServer.Serve(wsListener); err != nil && errors.Is(err, http.ErrServerClosed) {
if !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}
}()

// Initialize and start HTTP server
go func() {
if err := http.ListenAndServe("127.0.0.1:42070", mux); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}
}()

// Start event handling
go e2eApp.handleResultEvents(t)

return e2eApp
}

// Close gracefully shuts down the E2EApp and its servers
func (app *E2EApp) Close() error {
app.grpcServer.GracefulStop()
if err := app.wsServer.Close(); err != nil {
return err
}

close(app.resultEventChan)

return nil
}

// GetClientConn returns a gRPC client connection to the E2EApp's gRPC server.
func (app *E2EApp) GetClientConn() (*grpc.ClientConn, error) {
return grpc.NewClient(
app.grpcListener.Addr().String(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
}

// GetWSEndpoint returns the WebSocket endpoint URL
func (app *E2EApp) GetWSEndpoint() string {
return "ws://" + app.wsListener.Addr().String() + "/websocket"
}

// TODO_IN_THIS_COMMIT: godoc & move...

Choose a reason for hiding this comment

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

[linter-name (fail-on-found)] reported by reviewdog 🐶
// TODO_IN_THIS_COMMIT: godoc & move...

func (app *E2EApp) GetCometBlockID() comettypes.BlockID {
lastBlockID := app.GetSdkCtx().BlockHeader().LastBlockId
partSetHeader := lastBlockID.GetPartSetHeader()

return comettypes.BlockID{
Hash: lastBlockID.GetHash(),
PartSetHeader: comettypes.PartSetHeader{
Total: partSetHeader.GetTotal(),
Hash: partSetHeader.GetHash(),
},
}
}
110 changes: 110 additions & 0 deletions testutil/e2e/app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package e2e

import (
"testing"

"cosmossdk.io/depinject"
"cosmossdk.io/math"
comethttp "github.com/cometbft/cometbft/rpc/client/http"
cosmostx "github.com/cosmos/cosmos-sdk/client/tx"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
cosmostypes "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/stretchr/testify/require"

"github.com/pokt-network/poktroll/app/volatile"
"github.com/pokt-network/poktroll/pkg/client/block"
"github.com/pokt-network/poktroll/pkg/client/events"
"github.com/pokt-network/poktroll/pkg/client/query"
"github.com/pokt-network/poktroll/pkg/client/tx"
txtypes "github.com/pokt-network/poktroll/pkg/client/tx/types"
"github.com/pokt-network/poktroll/testutil/testclient"
gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
)

func TestNewE2EApp(t *testing.T) {
app := NewE2EApp(t)

// Construct dependencies...
keyRing := keyring.NewInMemory(app.GetCodec())
rec, err := keyRing.NewAccount(
"gateway2",
"suffer wet jelly furnace cousin flip layer render finish frequent pledge feature economy wink like water disease final erase goat include apple state furnace",
"",
cosmostypes.FullFundraiserPath,
hd.Secp256k1,
)
require.NoError(t, err)

gateway2Addr, err := rec.GetAddress()
require.NoError(t, err)

blockQueryClient, err := comethttp.New("tcp://127.0.0.1:42070", "/websocket")
require.NoError(t, err)

grpcConn, err := app.GetClientConn()
require.NoError(t, err)

deps := depinject.Supply(grpcConn, blockQueryClient)

sharedQueryClient, err := query.NewSharedQuerier(deps)
require.NoError(t, err)

sharedParams, err := sharedQueryClient.GetParams(app.GetSdkCtx())
require.NoError(t, err)
require.Equal(t, sharedtypes.DefaultParams(), *sharedParams)

eventsQueryClient := events.NewEventsQueryClient("ws://127.0.0.1:6969/websocket")
deps = depinject.Configs(deps, depinject.Supply(eventsQueryClient))
blockClient, err := block.NewBlockClient(app.GetSdkCtx(), deps)
require.NoError(t, err)

flagSet := testclient.NewFlagSet(t, "tcp://127.0.0.1:42070")
// DEV_NOTE: DO NOT use the clientCtx as a grpc.ClientConn as it bypasses E2EApp integrations.
clientCtx := testclient.NewLocalnetClientCtx(t, flagSet).WithKeyring(keyRing)

txFactory, err := cosmostx.NewFactoryCLI(clientCtx, flagSet)
require.NoError(t, err)

deps = depinject.Configs(deps, depinject.Supply(txtypes.Context(clientCtx), txFactory))

txContext, err := tx.NewTxContext(deps)
require.NoError(t, err)

deps = depinject.Configs(deps, depinject.Supply(blockClient, txContext))
txClient, err := tx.NewTxClient(app.GetSdkCtx(), deps, tx.WithSigningKeyName("gateway2"))
require.NoError(t, err)

// Assert that no gateways are staked.
gatewayQueryClient := gatewaytypes.NewQueryClient(grpcConn)
allGatewaysRes, err := gatewayQueryClient.AllGateways(app.GetSdkCtx(), &gatewaytypes.QueryAllGatewaysRequest{})
require.Equal(t, 0, len(allGatewaysRes.Gateways))

// Fund gateway2 account.
_, err = app.RunMsg(t, &banktypes.MsgSend{
FromAddress: app.GetFaucetBech32(),
ToAddress: gateway2Addr.String(),
Amount: cosmostypes.NewCoins(cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 10000000000)),
})
require.NoError(t, err)

// Stake gateway2.
eitherErr := txClient.SignAndBroadcast(
app.GetSdkCtx(),
gatewaytypes.NewMsgStakeGateway(
"pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz",
cosmostypes.NewCoin(volatile.DenomuPOKT, math.NewInt(100000001)),
),
)

err, errCh := eitherErr.SyncOrAsyncError()
require.NoError(t, err)
require.NoError(t, <-errCh)

// Assert that only gateway2 is staked.
allGatewaysRes, err = gatewayQueryClient.AllGateways(app.GetSdkCtx(), &gatewaytypes.QueryAllGatewaysRequest{})
require.Equal(t, 1, len(allGatewaysRes.Gateways))
require.Equal(t, "pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz", allGatewaysRes.Gateways[0].Address)
}
Loading

Unchanged files with check annotations Beta

"github.com/pokt-network/poktroll/app/volatile"
"github.com/pokt-network/poktroll/testutil/events"
"github.com/pokt-network/poktroll/testutil/integration/suites"

Check failure on line 13 in tests/integration/application/application_transfer_test.go

GitHub Actions / go-test

could not import github.com/pokt-network/poktroll/testutil/integration/suites (-: # github.com/pokt-network/poktroll/testutil/integration/suites
"github.com/pokt-network/poktroll/testutil/testkeyring"
apptypes "github.com/pokt-network/poktroll/x/application/types"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
// TestAppTransferSuite runs the application transfer test suite.
func TestAppTransferSuite(t *testing.T) {
suite.Run(t, new(appTransferTestSuite))

Check failure on line 45 in tests/integration/application/application_transfer_test.go

GitHub Actions / go-test

cannot use new(appTransferTestSuite) (value of type *appTransferTestSuite) as suite.TestingSuite value in argument to suite.Run: *appTransferTestSuite does not implement suite.TestingSuite (missing method SetS) (typecheck)
}
func (s *appTransferTestSuite) SetupTest() {
// Construct a new integration app for each test.
s.NewApp(s.T())

Check failure on line 50 in tests/integration/application/application_transfer_test.go

GitHub Actions / go-test

s.NewApp undefined (type *appTransferTestSuite has no field or method NewApp) (typecheck)
s.gatewaySuite.SetApp(s.GetApp())

Check failure on line 51 in tests/integration/application/application_transfer_test.go

GitHub Actions / go-test

s.GetApp undefined (type *appTransferTestSuite has no field or method GetApp) (typecheck)
s.paramsSuite.SetApp(s.GetApp())

Check failure on line 52 in tests/integration/application/application_transfer_test.go

GitHub Actions / go-test

s.GetApp undefined (type *appTransferTestSuite has no field or method GetApp) (typecheck)
// Setup authz accounts and grants to enable updating params.
s.paramsSuite.SetupTestAuthzAccounts(s.T())

Check failure on line 55 in tests/integration/application/application_transfer_test.go

GitHub Actions / go-test

s.T undefined (type *appTransferTestSuite has no field or method T) (typecheck)
s.paramsSuite.SetupTestAuthzGrants(s.T())

Check failure on line 56 in tests/integration/application/application_transfer_test.go

GitHub Actions / go-test

s.T undefined (type *appTransferTestSuite has no field or method T) (typecheck)
// Ensure gateways and apps have bank balances.
s.setupTestAddresses()
})
// Assert the on-chain state shows the application 3 as NOT staked.
_, queryErr := s.GetAppQueryClient().GetApplication(s.SdkCtx(), s.app3)

Check failure on line 84 in tests/integration/application/application_transfer_test.go

GitHub Actions / go-test

s.GetAppQueryClient undefined (type *appTransferTestSuite has no field or method GetAppQueryClient) (typecheck)
require.ErrorContains(s.T(), queryErr, "application not found")
require.ErrorContains(s.T(), queryErr, s.app3)
}
// GetBankQueryClient constructs and returns a query client for the bank module
// of the integration app.
func (s *BaseIntegrationSuite) GetBankQueryClient() banktypes.QueryClient {
return banktypes.NewQueryClient(s.GetApp().QueryHelper())

Check failure on line 101 in testutil/integration/suites/base.go

GitHub Actions / go-test

cannot use s.GetApp().QueryHelper() (value of type *baseapp.GRPCQueryRouter) as "github.com/cosmos/gogoproto/grpc".ClientConn value in argument to banktypes.NewQueryClient: *baseapp.GRPCQueryRouter does not implement "github.com/cosmos/gogoproto/grpc".ClientConn (missing method Invoke)
}
// FilterEvents returns the events from the event manager which match the given
s.RunAuthzGrantMsg(t, granterAddr, granteeAddr, authorization)
// Query for the created grant to assert that they were created.
authzQueryClient := authz.NewQueryClient(s.app.QueryHelper())

Check failure on line 46 in testutil/integration/suites/authz.go

GitHub Actions / go-test

cannot use s.app.QueryHelper() (value of type *baseapp.GRPCQueryRouter) as "github.com/cosmos/gogoproto/grpc".ClientConn value in argument to authz.NewQueryClient: *baseapp.GRPCQueryRouter does not implement "github.com/cosmos/gogoproto/grpc".ClientConn (missing method Invoke)) (typecheck)
queryGrantsReq := &authz.QueryGrantsRequest{
Granter: granterAddr.String(),
Grantee: granteeAddr.String(),