bitbucket.org/number571/tendermint@v0.8.14/state/indexer/sink/psql/psql_test.go (about)

     1  package psql
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"testing"
    11  	"time"
    12  
    13  	abci "bitbucket.org/number571/tendermint/abci/types"
    14  	"bitbucket.org/number571/tendermint/state/indexer"
    15  	"bitbucket.org/number571/tendermint/types"
    16  	sq "github.com/Masterminds/squirrel"
    17  	schema "github.com/adlio/schema"
    18  	proto "github.com/gogo/protobuf/proto"
    19  	_ "github.com/lib/pq"
    20  	dockertest "github.com/ory/dockertest"
    21  	"github.com/ory/dockertest/docker"
    22  	"github.com/stretchr/testify/assert"
    23  	"github.com/stretchr/testify/require"
    24  )
    25  
    26  var db *sql.DB
    27  var resource *dockertest.Resource
    28  var chainID = "test-chainID"
    29  
    30  var (
    31  	user     = "postgres"
    32  	password = "secret"
    33  	port     = "5432"
    34  	dsn      = "postgres://%s:%s@localhost:%s/%s?sslmode=disable"
    35  	dbName   = "postgres"
    36  )
    37  
    38  func TestType(t *testing.T) {
    39  	pool, err := setupDB(t)
    40  	require.NoError(t, err)
    41  
    42  	psqlSink := &EventSink{store: db, chainID: chainID}
    43  	assert.Equal(t, indexer.PSQL, psqlSink.Type())
    44  	require.NoError(t, teardown(t, pool))
    45  }
    46  
    47  func TestBlockFuncs(t *testing.T) {
    48  	pool, err := setupDB(t)
    49  	require.NoError(t, err)
    50  
    51  	indexer := &EventSink{store: db, chainID: chainID}
    52  	require.NoError(t, indexer.IndexBlockEvents(getTestBlockHeader()))
    53  
    54  	r, err := verifyBlock(1)
    55  	assert.True(t, r)
    56  	require.NoError(t, err)
    57  
    58  	r, err = verifyBlock(2)
    59  	assert.False(t, r)
    60  	require.NoError(t, err)
    61  
    62  	r, err = indexer.HasBlock(1)
    63  	assert.False(t, r)
    64  	assert.Equal(t, errors.New("hasBlock is not supported via the postgres event sink"), err)
    65  
    66  	r, err = indexer.HasBlock(2)
    67  	assert.False(t, r)
    68  	assert.Equal(t, errors.New("hasBlock is not supported via the postgres event sink"), err)
    69  
    70  	r2, err := indexer.SearchBlockEvents(context.TODO(), nil)
    71  	assert.Nil(t, r2)
    72  	assert.Equal(t, errors.New("block search is not supported via the postgres event sink"), err)
    73  
    74  	require.NoError(t, verifyTimeStamp(TableEventBlock))
    75  
    76  	// try to insert the duplicate block events.
    77  	err = indexer.IndexBlockEvents(getTestBlockHeader())
    78  	require.NoError(t, err)
    79  
    80  	require.NoError(t, teardown(t, pool))
    81  }
    82  
    83  func TestTxFuncs(t *testing.T) {
    84  	pool, err := setupDB(t)
    85  	assert.Nil(t, err)
    86  
    87  	indexer := &EventSink{store: db, chainID: chainID}
    88  
    89  	txResult := txResultWithEvents([]abci.Event{
    90  		{Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}},
    91  		{Type: "account", Attributes: []abci.EventAttribute{{Key: "owner", Value: "Ivan", Index: true}}},
    92  		{Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}},
    93  	})
    94  	err = indexer.IndexTxEvents([]*abci.TxResult{txResult})
    95  	require.NoError(t, err)
    96  
    97  	tx, err := verifyTx(types.Tx(txResult.Tx).Hash())
    98  	require.NoError(t, err)
    99  	assert.Equal(t, txResult, tx)
   100  
   101  	require.NoError(t, verifyTimeStamp(TableEventTx))
   102  	require.NoError(t, verifyTimeStamp(TableResultTx))
   103  
   104  	tx, err = indexer.GetTxByHash(types.Tx(txResult.Tx).Hash())
   105  	assert.Nil(t, tx)
   106  	assert.Equal(t, errors.New("getTxByHash is not supported via the postgres event sink"), err)
   107  
   108  	r2, err := indexer.SearchTxEvents(context.TODO(), nil)
   109  	assert.Nil(t, r2)
   110  	assert.Equal(t, errors.New("tx search is not supported via the postgres event sink"), err)
   111  
   112  	// try to insert the duplicate tx events.
   113  	err = indexer.IndexTxEvents([]*abci.TxResult{txResult})
   114  	require.NoError(t, err)
   115  
   116  	assert.Nil(t, teardown(t, pool))
   117  }
   118  
   119  func TestStop(t *testing.T) {
   120  	pool, err := setupDB(t)
   121  	require.NoError(t, err)
   122  
   123  	indexer := &EventSink{store: db}
   124  	require.NoError(t, indexer.Stop())
   125  
   126  	defer db.Close()
   127  	require.NoError(t, pool.Purge(resource))
   128  }
   129  
   130  func getTestBlockHeader() types.EventDataNewBlockHeader {
   131  	return types.EventDataNewBlockHeader{
   132  		Header: types.Header{Height: 1},
   133  		ResultBeginBlock: abci.ResponseBeginBlock{
   134  			Events: []abci.Event{
   135  				{
   136  					Type: "begin_event",
   137  					Attributes: []abci.EventAttribute{
   138  						{
   139  							Key:   "proposer",
   140  							Value: "FCAA001",
   141  							Index: true,
   142  						},
   143  					},
   144  				},
   145  			},
   146  		},
   147  		ResultEndBlock: abci.ResponseEndBlock{
   148  			Events: []abci.Event{
   149  				{
   150  					Type: "end_event",
   151  					Attributes: []abci.EventAttribute{
   152  						{
   153  							Key:   "foo",
   154  							Value: "100",
   155  							Index: true,
   156  						},
   157  					},
   158  				},
   159  			},
   160  		},
   161  	}
   162  }
   163  
   164  func readSchema() ([]*schema.Migration, error) {
   165  
   166  	filename := "schema.sql"
   167  	contents, err := ioutil.ReadFile(filename)
   168  	if err != nil {
   169  		return nil, fmt.Errorf("failed to read sql file from '%s': %w", filename, err)
   170  	}
   171  
   172  	mg := &schema.Migration{}
   173  	mg.ID = time.Now().Local().String() + " db schema"
   174  	mg.Script = string(contents)
   175  	return append([]*schema.Migration{}, mg), nil
   176  }
   177  
   178  func resetDB(t *testing.T) {
   179  	q := "DROP TABLE IF EXISTS block_events,tx_events,tx_results"
   180  	_, err := db.Exec(q)
   181  
   182  	require.NoError(t, err)
   183  
   184  	q = "DROP TYPE IF EXISTS block_event_type"
   185  	_, err = db.Exec(q)
   186  	require.NoError(t, err)
   187  }
   188  
   189  func txResultWithEvents(events []abci.Event) *abci.TxResult {
   190  	tx := types.Tx("HELLO WORLD")
   191  	return &abci.TxResult{
   192  		Height: 1,
   193  		Index:  0,
   194  		Tx:     tx,
   195  		Result: abci.ResponseDeliverTx{
   196  			Data:   []byte{0},
   197  			Code:   abci.CodeTypeOK,
   198  			Log:    "",
   199  			Events: events,
   200  		},
   201  	}
   202  }
   203  
   204  func verifyTx(hash []byte) (*abci.TxResult, error) {
   205  	join := fmt.Sprintf("%s ON %s.id = tx_result_id", TableEventTx, TableResultTx)
   206  	sqlStmt := sq.
   207  		Select("tx_result", fmt.Sprintf("%s.id", TableResultTx), "tx_result_id", "hash", "chain_id").
   208  		Distinct().From(TableResultTx).
   209  		InnerJoin(join).
   210  		Where(fmt.Sprintf("hash = $1 AND chain_id = '%s'", chainID), fmt.Sprintf("%X", hash))
   211  
   212  	rows, err := sqlStmt.RunWith(db).Query()
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	defer rows.Close()
   218  
   219  	if rows.Next() {
   220  		var txResult []byte
   221  		var txResultID, txid int
   222  		var h, cid string
   223  		err = rows.Scan(&txResult, &txResultID, &txid, &h, &cid)
   224  		if err != nil {
   225  			return nil, nil
   226  		}
   227  
   228  		msg := new(abci.TxResult)
   229  		err = proto.Unmarshal(txResult, msg)
   230  		if err != nil {
   231  			return nil, err
   232  		}
   233  
   234  		return msg, err
   235  	}
   236  
   237  	// No result
   238  	return nil, nil
   239  }
   240  
   241  func verifyTimeStamp(tb string) error {
   242  
   243  	// We assume the tx indexing time would not exceed 2 second from now
   244  	sqlStmt := sq.
   245  		Select(fmt.Sprintf("%s.created_at", tb)).
   246  		Distinct().From(tb).
   247  		Where(fmt.Sprintf("%s.created_at >= $1", tb), time.Now().Add(-2*time.Second))
   248  
   249  	rows, err := sqlStmt.RunWith(db).Query()
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	defer rows.Close()
   255  
   256  	if rows.Next() {
   257  		var ts string
   258  		err = rows.Scan(&ts)
   259  		if err != nil {
   260  			return err
   261  		}
   262  
   263  		return nil
   264  	}
   265  
   266  	return errors.New("no result")
   267  }
   268  
   269  func verifyBlock(h int64) (bool, error) {
   270  	sqlStmt := sq.
   271  		Select("height").
   272  		Distinct().
   273  		From(TableEventBlock).
   274  		Where(fmt.Sprintf("height = %d", h))
   275  	rows, err := sqlStmt.RunWith(db).Query()
   276  	if err != nil {
   277  		return false, err
   278  	}
   279  
   280  	defer rows.Close()
   281  
   282  	if !rows.Next() {
   283  		return false, nil
   284  	}
   285  
   286  	sqlStmt = sq.
   287  		Select("type, height", "chain_id").
   288  		Distinct().
   289  		From(TableEventBlock).
   290  		Where(fmt.Sprintf("height = %d AND type = '%s' AND chain_id = '%s'", h, types.EventTypeBeginBlock, chainID))
   291  
   292  	rows, err = sqlStmt.RunWith(db).Query()
   293  	if err != nil {
   294  		return false, err
   295  	}
   296  	defer rows.Close()
   297  
   298  	if !rows.Next() {
   299  		return false, nil
   300  	}
   301  
   302  	sqlStmt = sq.
   303  		Select("type, height").
   304  		Distinct().
   305  		From(TableEventBlock).
   306  		Where(fmt.Sprintf("height = %d AND type = '%s'", h, types.EventTypeEndBlock))
   307  	rows, err = sqlStmt.RunWith(db).Query()
   308  
   309  	if err != nil {
   310  		return false, err
   311  	}
   312  	defer rows.Close()
   313  
   314  	return rows.Next(), nil
   315  }
   316  
   317  func setupDB(t *testing.T) (*dockertest.Pool, error) {
   318  	t.Helper()
   319  	pool, err := dockertest.NewPool(os.Getenv("DOCKER_URL"))
   320  
   321  	require.NoError(t, err)
   322  
   323  	resource, err = pool.RunWithOptions(&dockertest.RunOptions{
   324  		Repository: DriverName,
   325  		Tag:        "13",
   326  		Env: []string{
   327  			"POSTGRES_USER=" + user,
   328  			"POSTGRES_PASSWORD=" + password,
   329  			"POSTGRES_DB=" + dbName,
   330  			"listen_addresses = '*'",
   331  		},
   332  		ExposedPorts: []string{port},
   333  	}, func(config *docker.HostConfig) {
   334  		// set AutoRemove to true so that stopped container goes away by itself
   335  		config.AutoRemove = true
   336  		config.RestartPolicy = docker.RestartPolicy{
   337  			Name: "no",
   338  		}
   339  	})
   340  
   341  	require.NoError(t, err)
   342  
   343  	// Set the container to expire in a minute to avoid orphaned containers
   344  	// hanging around
   345  	_ = resource.Expire(60)
   346  
   347  	conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName)
   348  
   349  	if err = pool.Retry(func() error {
   350  		var err error
   351  
   352  		_, db, err = NewEventSink(conn, chainID)
   353  
   354  		if err != nil {
   355  			return err
   356  		}
   357  
   358  		return db.Ping()
   359  	}); err != nil {
   360  		require.NoError(t, err)
   361  	}
   362  
   363  	resetDB(t)
   364  
   365  	sm, err := readSchema()
   366  	assert.Nil(t, err)
   367  	assert.Nil(t, schema.NewMigrator().Apply(db, sm))
   368  	return pool, nil
   369  }
   370  
   371  func teardown(t *testing.T, pool *dockertest.Pool) error {
   372  	t.Helper()
   373  	// When you're done, kill and remove the container
   374  	assert.Nil(t, pool.Purge(resource))
   375  	return db.Close()
   376  }