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