bitbucket.org/number571/tendermint@v0.8.14/state/indexer/sink/psql/psql_test.go (about) 1 package psql 2 3 import ( 4 "context" 5 "database/sql" 6 "errors" 7 "fmt" 8 "io/ioutil" 9 "os" 10 "testing" 11 "time" 12 13 abci "bitbucket.org/number571/tendermint/abci/types" 14 "bitbucket.org/number571/tendermint/state/indexer" 15 "bitbucket.org/number571/tendermint/types" 16 sq "github.com/Masterminds/squirrel" 17 schema "github.com/adlio/schema" 18 proto "github.com/gogo/protobuf/proto" 19 _ "github.com/lib/pq" 20 dockertest "github.com/ory/dockertest" 21 "github.com/ory/dockertest/docker" 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 ) 25 26 var db *sql.DB 27 var resource *dockertest.Resource 28 var chainID = "test-chainID" 29 30 var ( 31 user = "postgres" 32 password = "secret" 33 port = "5432" 34 dsn = "postgres://%s:%s@localhost:%s/%s?sslmode=disable" 35 dbName = "postgres" 36 ) 37 38 func TestType(t *testing.T) { 39 pool, err := setupDB(t) 40 require.NoError(t, err) 41 42 psqlSink := &EventSink{store: db, chainID: chainID} 43 assert.Equal(t, indexer.PSQL, psqlSink.Type()) 44 require.NoError(t, teardown(t, pool)) 45 } 46 47 func TestBlockFuncs(t *testing.T) { 48 pool, err := setupDB(t) 49 require.NoError(t, err) 50 51 indexer := &EventSink{store: db, chainID: chainID} 52 require.NoError(t, indexer.IndexBlockEvents(getTestBlockHeader())) 53 54 r, err := verifyBlock(1) 55 assert.True(t, r) 56 require.NoError(t, err) 57 58 r, err = verifyBlock(2) 59 assert.False(t, r) 60 require.NoError(t, err) 61 62 r, err = indexer.HasBlock(1) 63 assert.False(t, r) 64 assert.Equal(t, errors.New("hasBlock is not supported via the postgres event sink"), err) 65 66 r, err = indexer.HasBlock(2) 67 assert.False(t, r) 68 assert.Equal(t, errors.New("hasBlock is not supported via the postgres event sink"), err) 69 70 r2, err := indexer.SearchBlockEvents(context.TODO(), nil) 71 assert.Nil(t, r2) 72 assert.Equal(t, errors.New("block search is not supported via the postgres event sink"), err) 73 74 require.NoError(t, verifyTimeStamp(TableEventBlock)) 75 76 // try to insert the duplicate block events. 77 err = indexer.IndexBlockEvents(getTestBlockHeader()) 78 require.NoError(t, err) 79 80 require.NoError(t, teardown(t, pool)) 81 } 82 83 func TestTxFuncs(t *testing.T) { 84 pool, err := setupDB(t) 85 assert.Nil(t, err) 86 87 indexer := &EventSink{store: db, chainID: chainID} 88 89 txResult := txResultWithEvents([]abci.Event{ 90 {Type: "account", Attributes: []abci.EventAttribute{{Key: "number", Value: "1", Index: true}}}, 91 {Type: "account", Attributes: []abci.EventAttribute{{Key: "owner", Value: "Ivan", Index: true}}}, 92 {Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}}, 93 }) 94 err = indexer.IndexTxEvents([]*abci.TxResult{txResult}) 95 require.NoError(t, err) 96 97 tx, err := verifyTx(types.Tx(txResult.Tx).Hash()) 98 require.NoError(t, err) 99 assert.Equal(t, txResult, tx) 100 101 require.NoError(t, verifyTimeStamp(TableEventTx)) 102 require.NoError(t, verifyTimeStamp(TableResultTx)) 103 104 tx, err = indexer.GetTxByHash(types.Tx(txResult.Tx).Hash()) 105 assert.Nil(t, tx) 106 assert.Equal(t, errors.New("getTxByHash is not supported via the postgres event sink"), err) 107 108 r2, err := indexer.SearchTxEvents(context.TODO(), nil) 109 assert.Nil(t, r2) 110 assert.Equal(t, errors.New("tx search is not supported via the postgres event sink"), err) 111 112 // try to insert the duplicate tx events. 113 err = indexer.IndexTxEvents([]*abci.TxResult{txResult}) 114 require.NoError(t, err) 115 116 assert.Nil(t, teardown(t, pool)) 117 } 118 119 func TestStop(t *testing.T) { 120 pool, err := setupDB(t) 121 require.NoError(t, err) 122 123 indexer := &EventSink{store: db} 124 require.NoError(t, indexer.Stop()) 125 126 defer db.Close() 127 require.NoError(t, pool.Purge(resource)) 128 } 129 130 func getTestBlockHeader() types.EventDataNewBlockHeader { 131 return types.EventDataNewBlockHeader{ 132 Header: types.Header{Height: 1}, 133 ResultBeginBlock: abci.ResponseBeginBlock{ 134 Events: []abci.Event{ 135 { 136 Type: "begin_event", 137 Attributes: []abci.EventAttribute{ 138 { 139 Key: "proposer", 140 Value: "FCAA001", 141 Index: true, 142 }, 143 }, 144 }, 145 }, 146 }, 147 ResultEndBlock: abci.ResponseEndBlock{ 148 Events: []abci.Event{ 149 { 150 Type: "end_event", 151 Attributes: []abci.EventAttribute{ 152 { 153 Key: "foo", 154 Value: "100", 155 Index: true, 156 }, 157 }, 158 }, 159 }, 160 }, 161 } 162 } 163 164 func readSchema() ([]*schema.Migration, error) { 165 166 filename := "schema.sql" 167 contents, err := ioutil.ReadFile(filename) 168 if err != nil { 169 return nil, fmt.Errorf("failed to read sql file from '%s': %w", filename, err) 170 } 171 172 mg := &schema.Migration{} 173 mg.ID = time.Now().Local().String() + " db schema" 174 mg.Script = string(contents) 175 return append([]*schema.Migration{}, mg), nil 176 } 177 178 func resetDB(t *testing.T) { 179 q := "DROP TABLE IF EXISTS block_events,tx_events,tx_results" 180 _, err := db.Exec(q) 181 182 require.NoError(t, err) 183 184 q = "DROP TYPE IF EXISTS block_event_type" 185 _, err = db.Exec(q) 186 require.NoError(t, err) 187 } 188 189 func txResultWithEvents(events []abci.Event) *abci.TxResult { 190 tx := types.Tx("HELLO WORLD") 191 return &abci.TxResult{ 192 Height: 1, 193 Index: 0, 194 Tx: tx, 195 Result: abci.ResponseDeliverTx{ 196 Data: []byte{0}, 197 Code: abci.CodeTypeOK, 198 Log: "", 199 Events: events, 200 }, 201 } 202 } 203 204 func verifyTx(hash []byte) (*abci.TxResult, error) { 205 join := fmt.Sprintf("%s ON %s.id = tx_result_id", TableEventTx, TableResultTx) 206 sqlStmt := sq. 207 Select("tx_result", fmt.Sprintf("%s.id", TableResultTx), "tx_result_id", "hash", "chain_id"). 208 Distinct().From(TableResultTx). 209 InnerJoin(join). 210 Where(fmt.Sprintf("hash = $1 AND chain_id = '%s'", chainID), fmt.Sprintf("%X", hash)) 211 212 rows, err := sqlStmt.RunWith(db).Query() 213 if err != nil { 214 return nil, err 215 } 216 217 defer rows.Close() 218 219 if rows.Next() { 220 var txResult []byte 221 var txResultID, txid int 222 var h, cid string 223 err = rows.Scan(&txResult, &txResultID, &txid, &h, &cid) 224 if err != nil { 225 return nil, nil 226 } 227 228 msg := new(abci.TxResult) 229 err = proto.Unmarshal(txResult, msg) 230 if err != nil { 231 return nil, err 232 } 233 234 return msg, err 235 } 236 237 // No result 238 return nil, nil 239 } 240 241 func verifyTimeStamp(tb string) error { 242 243 // We assume the tx indexing time would not exceed 2 second from now 244 sqlStmt := sq. 245 Select(fmt.Sprintf("%s.created_at", tb)). 246 Distinct().From(tb). 247 Where(fmt.Sprintf("%s.created_at >= $1", tb), time.Now().Add(-2*time.Second)) 248 249 rows, err := sqlStmt.RunWith(db).Query() 250 if err != nil { 251 return err 252 } 253 254 defer rows.Close() 255 256 if rows.Next() { 257 var ts string 258 err = rows.Scan(&ts) 259 if err != nil { 260 return err 261 } 262 263 return nil 264 } 265 266 return errors.New("no result") 267 } 268 269 func verifyBlock(h int64) (bool, error) { 270 sqlStmt := sq. 271 Select("height"). 272 Distinct(). 273 From(TableEventBlock). 274 Where(fmt.Sprintf("height = %d", h)) 275 rows, err := sqlStmt.RunWith(db).Query() 276 if err != nil { 277 return false, err 278 } 279 280 defer rows.Close() 281 282 if !rows.Next() { 283 return false, nil 284 } 285 286 sqlStmt = sq. 287 Select("type, height", "chain_id"). 288 Distinct(). 289 From(TableEventBlock). 290 Where(fmt.Sprintf("height = %d AND type = '%s' AND chain_id = '%s'", h, types.EventTypeBeginBlock, chainID)) 291 292 rows, err = sqlStmt.RunWith(db).Query() 293 if err != nil { 294 return false, err 295 } 296 defer rows.Close() 297 298 if !rows.Next() { 299 return false, nil 300 } 301 302 sqlStmt = sq. 303 Select("type, height"). 304 Distinct(). 305 From(TableEventBlock). 306 Where(fmt.Sprintf("height = %d AND type = '%s'", h, types.EventTypeEndBlock)) 307 rows, err = sqlStmt.RunWith(db).Query() 308 309 if err != nil { 310 return false, err 311 } 312 defer rows.Close() 313 314 return rows.Next(), nil 315 } 316 317 func setupDB(t *testing.T) (*dockertest.Pool, error) { 318 t.Helper() 319 pool, err := dockertest.NewPool(os.Getenv("DOCKER_URL")) 320 321 require.NoError(t, err) 322 323 resource, err = pool.RunWithOptions(&dockertest.RunOptions{ 324 Repository: DriverName, 325 Tag: "13", 326 Env: []string{ 327 "POSTGRES_USER=" + user, 328 "POSTGRES_PASSWORD=" + password, 329 "POSTGRES_DB=" + dbName, 330 "listen_addresses = '*'", 331 }, 332 ExposedPorts: []string{port}, 333 }, func(config *docker.HostConfig) { 334 // set AutoRemove to true so that stopped container goes away by itself 335 config.AutoRemove = true 336 config.RestartPolicy = docker.RestartPolicy{ 337 Name: "no", 338 } 339 }) 340 341 require.NoError(t, err) 342 343 // Set the container to expire in a minute to avoid orphaned containers 344 // hanging around 345 _ = resource.Expire(60) 346 347 conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName) 348 349 if err = pool.Retry(func() error { 350 var err error 351 352 _, db, err = NewEventSink(conn, chainID) 353 354 if err != nil { 355 return err 356 } 357 358 return db.Ping() 359 }); err != nil { 360 require.NoError(t, err) 361 } 362 363 resetDB(t) 364 365 sm, err := readSchema() 366 assert.Nil(t, err) 367 assert.Nil(t, schema.NewMigrator().Apply(db, sm)) 368 return pool, nil 369 } 370 371 func teardown(t *testing.T, pool *dockertest.Pool) error { 372 t.Helper() 373 // When you're done, kill and remove the container 374 assert.Nil(t, pool.Purge(resource)) 375 return db.Close() 376 }