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 }