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 }