gitlab.com/gpdionisio/tendermint@v0.34.19-dev2/state/indexer/sink/psql/psql_test.go (about)

     1  package psql
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"flag"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"log"
    10  	"os"
    11  	"os/signal"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/adlio/schema"
    16  	"github.com/gogo/protobuf/proto"
    17  	"github.com/ory/dockertest"
    18  	"github.com/ory/dockertest/docker"
    19  	"github.com/stretchr/testify/assert"
    20  	"github.com/stretchr/testify/require"
    21  
    22  	abci "github.com/tendermint/tendermint/abci/types"
    23  	"github.com/tendermint/tendermint/types"
    24  
    25  	// Register the Postgres database driver.
    26  	_ "github.com/lib/pq"
    27  )
    28  
    29  var (
    30  	doPauseAtExit = flag.Bool("pause-at-exit", false,
    31  		"If true, pause the test until interrupted at shutdown, to allow debugging")
    32  
    33  	// A hook that test cases can call to obtain the shared database instance
    34  	// used for testing the sink. This is initialized in TestMain (see below).
    35  	testDB func() *sql.DB
    36  )
    37  
    38  const (
    39  	user     = "postgres"
    40  	password = "secret"
    41  	port     = "5432"
    42  	dsn      = "postgres://%s:%s@localhost:%s/%s?sslmode=disable"
    43  	dbName   = "postgres"
    44  	chainID  = "test-chainID"
    45  
    46  	viewBlockEvents = "block_events"
    47  	viewTxEvents    = "tx_events"
    48  )
    49  
    50  func TestMain(m *testing.M) {
    51  	flag.Parse()
    52  
    53  	// Set up docker and start a container running PostgreSQL.
    54  	pool, err := dockertest.NewPool(os.Getenv("DOCKER_URL"))
    55  	if err != nil {
    56  		log.Fatalf("Creating docker pool: %v", err)
    57  	}
    58  
    59  	resource, err := pool.RunWithOptions(&dockertest.RunOptions{
    60  		Repository: "postgres",
    61  		Tag:        "13",
    62  		Env: []string{
    63  			"POSTGRES_USER=" + user,
    64  			"POSTGRES_PASSWORD=" + password,
    65  			"POSTGRES_DB=" + dbName,
    66  			"listen_addresses = '*'",
    67  		},
    68  		ExposedPorts: []string{port},
    69  	}, func(config *docker.HostConfig) {
    70  		// set AutoRemove to true so that stopped container goes away by itself
    71  		config.AutoRemove = true
    72  		config.RestartPolicy = docker.RestartPolicy{
    73  			Name: "no",
    74  		}
    75  	})
    76  	if err != nil {
    77  		log.Fatalf("Starting docker pool: %v", err)
    78  	}
    79  
    80  	if *doPauseAtExit {
    81  		log.Print("Pause at exit is enabled, containers will not expire")
    82  	} else {
    83  		const expireSeconds = 60
    84  		_ = resource.Expire(expireSeconds)
    85  		log.Printf("Container expiration set to %d seconds", expireSeconds)
    86  	}
    87  
    88  	// Connect to the database, clear any leftover data, and install the
    89  	// indexing schema.
    90  	conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName)
    91  	var db *sql.DB
    92  
    93  	if err := pool.Retry(func() error {
    94  		sink, err := NewEventSink(conn, chainID)
    95  		if err != nil {
    96  			return err
    97  		}
    98  		db = sink.DB() // set global for test use
    99  		return db.Ping()
   100  	}); err != nil {
   101  		log.Fatalf("Connecting to database: %v", err)
   102  	}
   103  
   104  	if err := resetDatabase(db); err != nil {
   105  		log.Fatalf("Flushing database: %v", err)
   106  	}
   107  
   108  	sm, err := readSchema()
   109  	if err != nil {
   110  		log.Fatalf("Reading schema: %v", err)
   111  	}
   112  	migrator := schema.NewMigrator()
   113  	if err := migrator.Apply(db, sm); err != nil {
   114  		log.Fatalf("Applying schema: %v", err)
   115  	}
   116  
   117  	// Set up the hook for tests to get the shared database handle.
   118  	testDB = func() *sql.DB { return db }
   119  
   120  	// Run the selected test cases.
   121  	code := m.Run()
   122  
   123  	// Clean up and shut down the database container.
   124  	if *doPauseAtExit {
   125  		log.Print("Testing complete, pausing for inspection. Send SIGINT to resume teardown")
   126  		waitForInterrupt()
   127  		log.Print("(resuming)")
   128  	}
   129  	log.Print("Shutting down database")
   130  	if err := pool.Purge(resource); err != nil {
   131  		log.Printf("WARNING: Purging pool failed: %v", err)
   132  	}
   133  	if err := db.Close(); err != nil {
   134  		log.Printf("WARNING: Closing database failed: %v", err)
   135  	}
   136  
   137  	os.Exit(code)
   138  }
   139  
   140  func TestIndexing(t *testing.T) {
   141  	t.Run("IndexBlockEvents", func(t *testing.T) {
   142  		indexer := &EventSink{store: testDB(), chainID: chainID}
   143  		require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
   144  
   145  		verifyBlock(t, 1)
   146  		verifyBlock(t, 2)
   147  
   148  		verifyNotImplemented(t, "hasBlock", func() (bool, error) { return indexer.HasBlock(1) })
   149  		verifyNotImplemented(t, "hasBlock", func() (bool, error) { return indexer.HasBlock(2) })
   150  
   151  		verifyNotImplemented(t, "block search", func() (bool, error) {
   152  			v, err := indexer.SearchBlockEvents(context.Background(), nil)
   153  			return v != nil, err
   154  		})
   155  
   156  		require.NoError(t, verifyTimeStamp(tableBlocks))
   157  
   158  		// Attempting to reindex the same events should gracefully succeed.
   159  		require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
   160  	})
   161  
   162  	t.Run("IndexTxEvents", func(t *testing.T) {
   163  		indexer := &EventSink{store: testDB(), chainID: chainID}
   164  
   165  		txResult := txResultWithEvents([]abci.Event{
   166  			makeIndexedEvent("account.number", "1"),
   167  			makeIndexedEvent("account.owner", "Ivan"),
   168  			makeIndexedEvent("account.owner", "Yulieta"),
   169  
   170  			{Type: "", Attributes: []abci.EventAttribute{
   171  				{
   172  					Key:   []byte("not_allowed"),
   173  					Value: []byte("Vlad"),
   174  					Index: true,
   175  				},
   176  			}},
   177  		})
   178  		require.NoError(t, indexer.IndexTxEvents([]*abci.TxResult{txResult}))
   179  
   180  		txr, err := loadTxResult(types.Tx(txResult.Tx).Hash())
   181  		require.NoError(t, err)
   182  		assert.Equal(t, txResult, txr)
   183  
   184  		require.NoError(t, verifyTimeStamp(tableTxResults))
   185  		require.NoError(t, verifyTimeStamp(viewTxEvents))
   186  
   187  		verifyNotImplemented(t, "getTxByHash", func() (bool, error) {
   188  			txr, err := indexer.GetTxByHash(types.Tx(txResult.Tx).Hash())
   189  			return txr != nil, err
   190  		})
   191  		verifyNotImplemented(t, "tx search", func() (bool, error) {
   192  			txr, err := indexer.SearchTxEvents(context.Background(), nil)
   193  			return txr != nil, err
   194  		})
   195  
   196  		// try to insert the duplicate tx events.
   197  		err = indexer.IndexTxEvents([]*abci.TxResult{txResult})
   198  		require.NoError(t, err)
   199  	})
   200  }
   201  
   202  func TestStop(t *testing.T) {
   203  	indexer := &EventSink{store: testDB()}
   204  	require.NoError(t, indexer.Stop())
   205  }
   206  
   207  // newTestBlockHeader constructs a fresh copy of a block header containing
   208  // known test values to exercise the indexer.
   209  func newTestBlockHeader() types.EventDataNewBlockHeader {
   210  	return types.EventDataNewBlockHeader{
   211  		Header: types.Header{Height: 1},
   212  		ResultBeginBlock: abci.ResponseBeginBlock{
   213  			Events: []abci.Event{
   214  				makeIndexedEvent("begin_event.proposer", "FCAA001"),
   215  				makeIndexedEvent("thingy.whatzit", "O.O"),
   216  			},
   217  		},
   218  		ResultEndBlock: abci.ResponseEndBlock{
   219  			Events: []abci.Event{
   220  				makeIndexedEvent("end_event.foo", "100"),
   221  				makeIndexedEvent("thingy.whatzit", "-.O"),
   222  			},
   223  		},
   224  	}
   225  }
   226  
   227  // readSchema loads the indexing database schema file
   228  func readSchema() ([]*schema.Migration, error) {
   229  	const filename = "schema.sql"
   230  	contents, err := ioutil.ReadFile(filename)
   231  	if err != nil {
   232  		return nil, fmt.Errorf("failed to read sql file from '%s': %w", filename, err)
   233  	}
   234  
   235  	return []*schema.Migration{{
   236  		ID:     time.Now().Local().String() + " db schema",
   237  		Script: string(contents),
   238  	}}, nil
   239  }
   240  
   241  // resetDB drops all the data from the test database.
   242  func resetDatabase(db *sql.DB) error {
   243  	_, err := db.Exec(`DROP TABLE IF EXISTS blocks,tx_results,events,attributes CASCADE;`)
   244  	if err != nil {
   245  		return fmt.Errorf("dropping tables: %v", err)
   246  	}
   247  	_, err = db.Exec(`DROP VIEW IF EXISTS event_attributes,block_events,tx_events CASCADE;`)
   248  	if err != nil {
   249  		return fmt.Errorf("dropping views: %v", err)
   250  	}
   251  	return nil
   252  }
   253  
   254  // txResultWithEvents constructs a fresh transaction result with fixed values
   255  // for testing, that includes the specified events.
   256  func txResultWithEvents(events []abci.Event) *abci.TxResult {
   257  	return &abci.TxResult{
   258  		Height: 1,
   259  		Index:  0,
   260  		Tx:     types.Tx("HELLO WORLD"),
   261  		Result: abci.ResponseDeliverTx{
   262  			Data:   []byte{0},
   263  			Code:   abci.CodeTypeOK,
   264  			Log:    "",
   265  			Events: events,
   266  		},
   267  	}
   268  }
   269  
   270  func loadTxResult(hash []byte) (*abci.TxResult, error) {
   271  	hashString := fmt.Sprintf("%X", hash)
   272  	var resultData []byte
   273  	if err := testDB().QueryRow(`
   274  SELECT tx_result FROM `+tableTxResults+` WHERE tx_hash = $1;
   275  `, hashString).Scan(&resultData); err != nil {
   276  		return nil, fmt.Errorf("lookup transaction for hash %q failed: %v", hashString, err)
   277  	}
   278  
   279  	txr := new(abci.TxResult)
   280  	if err := proto.Unmarshal(resultData, txr); err != nil {
   281  		return nil, fmt.Errorf("unmarshaling txr: %v", err)
   282  	}
   283  
   284  	return txr, nil
   285  }
   286  
   287  func verifyTimeStamp(tableName string) error {
   288  	return testDB().QueryRow(fmt.Sprintf(`
   289  SELECT DISTINCT %[1]s.created_at
   290    FROM %[1]s
   291    WHERE %[1]s.created_at >= $1;
   292  `, tableName), time.Now().Add(-2*time.Second)).Err()
   293  }
   294  
   295  func verifyBlock(t *testing.T, height int64) {
   296  	// Check that the blocks table contains an entry for this height.
   297  	if err := testDB().QueryRow(`
   298  SELECT height FROM `+tableBlocks+` WHERE height = $1;
   299  `, height).Err(); err == sql.ErrNoRows {
   300  		t.Errorf("No block found for height=%d", height)
   301  	} else if err != nil {
   302  		t.Fatalf("Database query failed: %v", err)
   303  	}
   304  
   305  	// Verify the presence of begin_block and end_block events.
   306  	if err := testDB().QueryRow(`
   307  SELECT type, height, chain_id FROM `+viewBlockEvents+`
   308    WHERE height = $1 AND type = $2 AND chain_id = $3;
   309  `, height, eventTypeBeginBlock, chainID).Err(); err == sql.ErrNoRows {
   310  		t.Errorf("No %q event found for height=%d", eventTypeBeginBlock, height)
   311  	} else if err != nil {
   312  		t.Fatalf("Database query failed: %v", err)
   313  	}
   314  
   315  	if err := testDB().QueryRow(`
   316  SELECT type, height, chain_id FROM `+viewBlockEvents+`
   317    WHERE height = $1 AND type = $2 AND chain_id = $3;
   318  `, height, eventTypeEndBlock, chainID).Err(); err == sql.ErrNoRows {
   319  		t.Errorf("No %q event found for height=%d", eventTypeEndBlock, height)
   320  	} else if err != nil {
   321  		t.Fatalf("Database query failed: %v", err)
   322  	}
   323  }
   324  
   325  // verifyNotImplemented calls f and verifies that it returns both a
   326  // false-valued flag and a non-nil error whose string matching the expected
   327  // "not supported" message with label prefixed.
   328  func verifyNotImplemented(t *testing.T, label string, f func() (bool, error)) {
   329  	t.Helper()
   330  	t.Logf("Verifying that %q reports it is not implemented", label)
   331  
   332  	want := label + " is not supported via the postgres event sink"
   333  	ok, err := f()
   334  	assert.False(t, ok)
   335  	require.NotNil(t, err)
   336  	assert.Equal(t, want, err.Error())
   337  }
   338  
   339  // waitForInterrupt blocks until a SIGINT is received by the process.
   340  func waitForInterrupt() {
   341  	ch := make(chan os.Signal, 1)
   342  	signal.Notify(ch, os.Interrupt)
   343  	<-ch
   344  }