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