synapsecns/sanguine

View on GitHub
ethergo/submitter/suite_test.go

Summary

Maintainability
A
35 mins
Test Coverage
package submitter_test

import (
    "context"
    "database/sql"
    "fmt"
    "math/big"
    "os"
    "sync"
    "testing"
    "time"

    "github.com/synapsecns/sanguine/ethergo/examples/contracttests"

    "github.com/Flaque/filet"
    "github.com/brianvoe/gofakeit/v6"
    "github.com/ethereum/go-ethereum/accounts/keystore"
    "github.com/ethereum/go-ethereum/params"
    "github.com/ipfs/go-log"
    "github.com/synapsecns/sanguine/core"
    "github.com/synapsecns/sanguine/core/config"
    common_base "github.com/synapsecns/sanguine/core/dbcommon"
    "github.com/synapsecns/sanguine/core/metrics"
    "github.com/synapsecns/sanguine/core/metrics/localmetrics"
    "github.com/synapsecns/sanguine/core/processlog"
    "github.com/synapsecns/sanguine/ethergo/backends/anvil"
    "github.com/synapsecns/sanguine/ethergo/backends/simulated"
    "github.com/synapsecns/sanguine/ethergo/client"
    "github.com/synapsecns/sanguine/ethergo/manager"
    "github.com/synapsecns/sanguine/ethergo/mocks"
    "github.com/synapsecns/sanguine/ethergo/signer/nonce"
    "github.com/synapsecns/sanguine/ethergo/signer/signer"
    "github.com/synapsecns/sanguine/ethergo/signer/signer/localsigner"
    "github.com/synapsecns/sanguine/ethergo/submitter"
    "github.com/synapsecns/sanguine/ethergo/submitter/db"
    "github.com/synapsecns/sanguine/ethergo/submitter/db/txdb"
    "gorm.io/driver/mysql"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/schema"

    "github.com/stretchr/testify/suite"
    "github.com/synapsecns/sanguine/core/testsuite"
    "github.com/synapsecns/sanguine/ethergo/backends"
)

var buildInfo = config.NewBuildInfo(config.DefaultVersion, config.DefaultCommit, "submitter", config.DefaultDate)
var fundAmount = new(big.Int).Mul(new(big.Int).SetUint64(uint64(params.Ether)), big.NewInt(10))

// SubmitterSuite is used to test individual contract deployments to make sure other tests don't break.
type SubmitterSuite struct {
    *testsuite.TestSuite
    // testBackend is the test backend
    testBackends []backends.SimulatedTestBackend
    // metrics is the metrics handler
    metrics metrics.Handler
    // signer is the signer to use for the test
    signer signer.Signer
    // store is the store to use for the test
    store db.Service
    // registry is the registry to use for the test
    deployer *manager.DeployerManager
    // localAccount is the local account to use for the test
    localAccount *keystore.Key
}

// GetClient returns a client for the given chain id.
func (s *SubmitterSuite) GetClient(ctx context.Context, chainID *big.Int) (client.EVM, error) {
    for _, backend := range s.testBackends {
        if backend.GetBigChainID().Cmp(chainID) == 0 {
            //nolint: wrapcheck
            return client.DialBackend(ctx, backend.RPCAddress(), s.metrics)
        }
    }
    return nil, fmt.Errorf("could not find client for chain id %v", chainID)
}

// SetupSuite sets up 3 backends and metrics.
func (s *SubmitterSuite) SetupSuite() {
    s.TestSuite.SetupSuite()

    testChainIDs := []uint64{1, 3, 4}
    s.testBackends = make([]backends.SimulatedTestBackend, len(testChainIDs))
    s.deployer = manager.NewDeployerManager(s.T(), contracttests.NewCounterDeployer)

    var wg sync.WaitGroup
    // wait for all the backends to be created, add 1 to the wait group for the metrics
    wg.Add(len(testChainIDs) + 1)

    logDir := filet.TmpDir(s.T(), "")

    // create the jaeger instance
    go func() {
        defer wg.Done()
        var err error
        // don't use metrics on ci for integration tests
        isCI := core.GetEnvBool("CI", false)
        useMetrics := !isCI
        metricsHandler := metrics.Null

        if useMetrics {
            localmetrics.SetupTestJaeger(s.GetSuiteContext(), s.T())
            metricsHandler = metrics.Jaeger
        }
        s.metrics, err = metrics.NewByType(s.GetSuiteContext(), buildInfo, metricsHandler)
        s.Require().NoError(err)
    }()

    // create the backends
    for i, chainID := range testChainIDs {
        go func(index int, chainID uint64) {
            defer wg.Done()
            options := anvil.NewAnvilOptionBuilder()
            options.SetChainID(chainID)
            // make sure all the docker containers log to the same directory
            options.SetProcessLogOptions(processlog.WithLogFileName(fmt.Sprintf("chain-%d.log", chainID)), processlog.WithLogDir(logDir))

            var err error
            s.testBackends[index], err = anvil.NewAnvilBackend(s.GetSuiteContext(), s.T(), options)
            s.Require().NoError(err)
            s.deployer.Get(s.GetSuiteContext(), s.testBackends[index], contracttests.CounterType)
        }(i, chainID)
    }
    wg.Wait()
}

// SetupTest sets up the signer and funds the account with 10 eth on each backend.
func (s *SubmitterSuite) SetupTest() {
    s.TestSuite.SetupTest()
    s.localAccount = mocks.MockAccount(s.T())
    // create the local signer
    s.signer = localsigner.NewSigner(s.localAccount.PrivateKey)
    var wg sync.WaitGroup
    wg.Add(len(s.testBackends) + 1)

    // setup the db
    go func() {
        defer wg.Done()
        var err error
        s.store, err = NewSqliteStore(s.GetTestContext(), filet.TmpDir(s.T(), ""), s.metrics)
        s.Require().NoError(err)
    }()

    // fund the account on each chain
    for i := range s.testBackends {
        go func(index int) {
            defer wg.Done()

            s.testBackends[index].FundAccount(s.GetTestContext(), s.signer.Address(), *fundAmount)
        }(i)
    }
    wg.Wait()
}

var _ submitter.ClientFetcher = &SubmitterSuite{}

// NewSubmitterSuite creates a end-to-end test suite.
func NewSubmitterSuite(tb testing.TB) *SubmitterSuite {
    tb.Helper()
    return &SubmitterSuite{
        TestSuite: testsuite.NewTestSuite(tb),
    }
}

func TestSubmitterSuite(t *testing.T) {
    suite.Run(t, NewSubmitterSuite(t))
}

// TXSubmitterDBSuite is used to test db queries across mysql and sqlite
// this is ran here rather than in the db package to avoid having to export the sqlite
// setup query to avoid confusion about how to use the library.
type TXSubmitterDBSuite struct {
    *testsuite.TestSuite
    dbs          []db.Service
    metrics      metrics.Handler
    testBackends []backends.SimulatedTestBackend
    managers     map[uint]nonce.Manager
    mockAccounts []*keystore.Key
}

func NewTXSubmitterDBSuite(tb testing.TB) *TXSubmitterDBSuite {
    tb.Helper()
    return &TXSubmitterDBSuite{
        TestSuite: testsuite.NewTestSuite(tb),
        dbs:       []db.Service{},
    }
}

func TestTXSubmitterDBSuite(t *testing.T) {
    suite.Run(t, NewTXSubmitterDBSuite(t))
}

func (t *TXSubmitterDBSuite) SetupSuite() {
    t.TestSuite.SetupSuite()
    // don't use metrics on ci for integration tests
    isCI := core.GetEnvBool("CI", false)
    useMetrics := !isCI
    metricsHandler := metrics.Null

    if useMetrics {
        localmetrics.SetupTestJaeger(t.GetSuiteContext(), t.T())
        metricsHandler = metrics.Jaeger
    }
    var err error
    t.metrics, err = metrics.NewByType(t.GetSuiteContext(), buildInfo, metricsHandler)
    t.Require().NoError(err)
}

func (t *TXSubmitterDBSuite) SetupTest() {
    t.TestSuite.SetupTest()

    t.dbs = []db.Service{}
    t.testBackends = []backends.SimulatedTestBackend{}

    t.setupMysqlDB()

    sqliteStore, err := NewSqliteStore(t.GetTestContext(), filet.TmpDir(t.T(), ""), t.metrics)
    t.Require().NoError(err)

    t.dbs = append(t.dbs, sqliteStore)

    // setup a signer for each chain
    t.managers = make(map[uint]nonce.Manager)

    // fund the accounts on each chain
    const mockAccounts = 5
    t.mockAccounts = make([]*keystore.Key, mockAccounts)
    for i := 0; i < mockAccounts; i++ {
        t.mockAccounts[i] = mocks.MockAccount(t.T())
    }

    chainIDs := []uint{1, 3, 4}
    var backendMux sync.Mutex

    var wg sync.WaitGroup
    wg.Add(len(chainIDs))

    for _, chainID := range chainIDs {
        go func(chainID uint) {
            defer wg.Done()

            backend := simulated.NewSimulatedBackendWithChainID(t.GetTestContext(), t.T(), big.NewInt(int64(chainID)))

            backendMux.Lock()
            t.testBackends = append(t.testBackends, backend)
            t.managers[chainID] = nonce.NewNonceManager(t.GetTestContext(), backend, backend.GetBigChainID())
            backendMux.Unlock()

            for _, account := range t.mockAccounts {
                backend.FundAccount(t.GetTestContext(), account.Address, *fundAmount)
            }
        }(chainID)
    }
    wg.Wait()
}

func (t *TXSubmitterDBSuite) RunOnAllDBs(testFunc func(db db.Service)) {
    wg := sync.WaitGroup{}
    for _, testDB := range t.dbs {
        wg.Add(1)
        // capture the value
        go func(testDB db.Service) {
            defer wg.Done()
            testFunc(testDB)
        }(testDB)
    }
    wg.Wait()
}

func (t *TXSubmitterDBSuite) setupMysqlDB() {
    // skip if mysql test disabled, this really only needs to be run in ci

    // skip if mysql test disabled
    if os.Getenv(common_base.EnableMysqlTestVar) == "" {
        return
    }
    // sets up the conn string to the default database
    connString := t.connString(os.Getenv(common_base.MysqlDatabaseVar))
    // sets up the myqsl db
    testDB, err := sql.Open("mysql", connString)
    t.Require().NoError(err)
    // close the db once the connection is don
    defer func() {
        t.Require().NoError(testDB.Close())
    }()

    mysqlStore, err := NewMysqlStore(t.GetTestContext(), connString, t.metrics)
    t.Require().NoError(err)

    t.dbs = append(t.dbs, mysqlStore)
}

// connString gets the mysql connection string.
func (t *TXSubmitterDBSuite) connString(dbname string) string {
    return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", core.GetEnv("MYSQL_USER", "root"), os.Getenv("MYSQL_PASSWORD"), core.GetEnv("MYSQL_HOST", "127.0.0.1"), core.GetEnvInt("MYSQL_PORT", 3306), dbname)
}

// Store wraps the store. Since tx submitter is a library and not a standalone service, we simulate db creation here.
// and then proceed as we would with any other db test.
type Store struct {
    *txdb.Store
}

// NewMysqlStore creates a new mysql data store. It emulates the way another caller would create a store.
func NewMysqlStore(parentCtx context.Context, dbURL string, handler metrics.Handler) (_ *Store, err error) {
    logger := log.Logger("mysql-store")

    ctx, span := handler.Tracer().Start(parentCtx, "start-mysql")
    defer func() {
        metrics.EndSpanWithErr(span, err)
    }()

    namingStrategy := schema.NamingStrategy{
        TablePrefix: fmt.Sprintf("test%d_%d_", gofakeit.Int64(), time.Now().Unix()),
    }

    gdb, err := gorm.Open(mysql.Open(dbURL), &gorm.Config{
        Logger:                 common_base.GetGormLogger(logger),
        FullSaveAssociations:   true,
        NamingStrategy:         namingStrategy,
        NowFunc:                time.Now,
        SkipDefaultTransaction: true,
    })

    if err != nil {
        return nil, fmt.Errorf("could not create mysql connection: %w", err)
    }

    sqlDB, err := gdb.DB()
    if err != nil {
        return nil, fmt.Errorf("could not get sql db: %w", err)
    }

    // fixes a timeout issue https://stackoverflow.com/a/42146536
    sqlDB.SetMaxIdleConns(10)
    sqlDB.SetConnMaxLifetime(time.Hour)

    handler.AddGormCallbacks(gdb)

    // migrate in a transaction since we skip this by default
    err = gdb.Transaction(func(tx *gorm.DB) error {
        //nolint: wrapcheck
        return gdb.WithContext(ctx).AutoMigrate(txdb.GetAllModels()...)
    })

    if err != nil {
        return nil, fmt.Errorf("could not migrate on mysql: %w", err)
    }
    return &Store{txdb.NewTXStore(gdb, handler)}, nil
}

// NewSqliteStore creates a new sqlite data store.
func NewSqliteStore(parentCtx context.Context, dbPath string, handler metrics.Handler) (_ *Store, err error) {
    logger := log.Logger("sqlite-store")

    logger.Debugf("creating sqlite store at %s", dbPath)

    ctx, span := handler.Tracer().Start(parentCtx, "start-sqlite")
    defer func() {
        metrics.EndSpanWithErr(span, err)
    }()

    // create the directory to the store if it doesn't exist
    err = os.MkdirAll(dbPath, os.ModePerm)
    if err != nil {
        return nil, fmt.Errorf("could not create sqlite store")
    }

    logger.Warnf("submitter database is at %s/synapse.db", dbPath)

    namingStrategy := schema.NamingStrategy{
        TablePrefix: fmt.Sprintf("test%d_%d_", gofakeit.Int64(), time.Now().Unix()),
    }

    gdb, err := gorm.Open(sqlite.Open(fmt.Sprintf("%s/%s", dbPath, "synapse.db")), &gorm.Config{
        DisableForeignKeyConstraintWhenMigrating: true,
        Logger:                                   common_base.GetGormLogger(logger),
        FullSaveAssociations:                     true,
        SkipDefaultTransaction:                   true,
        NamingStrategy:                           namingStrategy,
    })
    if err != nil {
        return nil, fmt.Errorf("could not connect to db %s: %w", dbPath, err)
    }

    handler.AddGormCallbacks(gdb)

    err = gdb.WithContext(ctx).AutoMigrate(txdb.GetAllModels()...)
    if err != nil {
        return nil, fmt.Errorf("could not migrate models: %w", err)
    }
    return &Store{txdb.NewTXStore(gdb, handler)}, nil
}