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