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 }