github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/internal/state/indexer/sink/psql/psql_test.go (about)

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