You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

352 lines
10 KiB

  1. package psql
  2. import (
  3. "context"
  4. "database/sql"
  5. "flag"
  6. "fmt"
  7. "log"
  8. "os"
  9. "os/signal"
  10. "testing"
  11. "time"
  12. "github.com/adlio/schema"
  13. "github.com/gogo/protobuf/proto"
  14. "github.com/ory/dockertest"
  15. "github.com/ory/dockertest/docker"
  16. "github.com/stretchr/testify/assert"
  17. "github.com/stretchr/testify/require"
  18. abci "github.com/tendermint/tendermint/abci/types"
  19. "github.com/tendermint/tendermint/internal/state/indexer"
  20. "github.com/tendermint/tendermint/types"
  21. // Register the Postgres database driver.
  22. _ "github.com/lib/pq"
  23. )
  24. // Verify that the type satisfies the EventSink interface.
  25. var _ indexer.EventSink = (*EventSink)(nil)
  26. var (
  27. doPauseAtExit = flag.Bool("pause-at-exit", false,
  28. "If true, pause the test until interrupted at shutdown, to allow debugging")
  29. // A hook that test cases can call to obtain the shared database instance
  30. // used for testing the sink. This is initialized in TestMain (see below).
  31. testDB func() *sql.DB
  32. )
  33. const (
  34. user = "postgres"
  35. password = "secret"
  36. port = "5432"
  37. dsn = "postgres://%s:%s@localhost:%s/%s?sslmode=disable"
  38. dbName = "postgres"
  39. chainID = "test-chainID"
  40. viewBlockEvents = "block_events"
  41. viewTxEvents = "tx_events"
  42. )
  43. func TestMain(m *testing.M) {
  44. flag.Parse()
  45. // Set up docker.
  46. pool, err := dockertest.NewPool(os.Getenv("DOCKER_URL"))
  47. if err != nil {
  48. log.Fatalf("Creating docker pool: %v", err)
  49. }
  50. // If docker is unavailable, log and exit without reporting failure.
  51. if _, err := pool.Client.Info(); err != nil {
  52. log.Printf("WARNING: Docker is not available: %v [skipping this test]", err)
  53. return
  54. }
  55. // Start a container running PostgreSQL.
  56. resource, err := pool.RunWithOptions(&dockertest.RunOptions{
  57. Repository: "postgres",
  58. Tag: "13",
  59. Env: []string{
  60. "POSTGRES_USER=" + user,
  61. "POSTGRES_PASSWORD=" + password,
  62. "POSTGRES_DB=" + dbName,
  63. "listen_addresses = '*'",
  64. },
  65. ExposedPorts: []string{port},
  66. }, func(config *docker.HostConfig) {
  67. // set AutoRemove to true so that stopped container goes away by itself
  68. config.AutoRemove = true
  69. config.RestartPolicy = docker.RestartPolicy{
  70. Name: "no",
  71. }
  72. })
  73. if err != nil {
  74. log.Fatalf("Starting docker pool: %v", err)
  75. }
  76. if *doPauseAtExit {
  77. log.Print("Pause at exit is enabled, containers will not expire")
  78. } else {
  79. const expireSeconds = 60
  80. _ = resource.Expire(expireSeconds)
  81. log.Printf("Container expiration set to %d seconds", expireSeconds)
  82. }
  83. // Connect to the database, clear any leftover data, and install the
  84. // indexing schema.
  85. conn := fmt.Sprintf(dsn, user, password, resource.GetPort(port+"/tcp"), dbName)
  86. var db *sql.DB
  87. if err := pool.Retry(func() error {
  88. sink, err := NewEventSink(conn, chainID)
  89. if err != nil {
  90. return err
  91. }
  92. db = sink.DB() // set global for test use
  93. return db.Ping()
  94. }); err != nil {
  95. log.Fatalf("Connecting to database: %v", err)
  96. }
  97. if err := resetDatabase(db); err != nil {
  98. log.Fatalf("Flushing database: %v", err)
  99. }
  100. sm, err := readSchema()
  101. if err != nil {
  102. log.Fatalf("Reading schema: %v", err)
  103. }
  104. migrator := schema.NewMigrator()
  105. if err := migrator.Apply(db, sm); err != nil {
  106. log.Fatalf("Applying schema: %v", err)
  107. }
  108. // Set up the hook for tests to get the shared database handle.
  109. testDB = func() *sql.DB { return db }
  110. // Run the selected test cases.
  111. code := m.Run()
  112. // Clean up and shut down the database container.
  113. if *doPauseAtExit {
  114. log.Print("Testing complete, pausing for inspection. Send SIGINT to resume teardown")
  115. waitForInterrupt()
  116. log.Print("(resuming)")
  117. }
  118. log.Print("Shutting down database")
  119. if err := pool.Purge(resource); err != nil {
  120. log.Printf("WARNING: Purging pool failed: %v", err)
  121. }
  122. if err := db.Close(); err != nil {
  123. log.Printf("WARNING: Closing database failed: %v", err)
  124. }
  125. os.Exit(code)
  126. }
  127. func TestType(t *testing.T) {
  128. psqlSink := &EventSink{store: testDB(), chainID: chainID}
  129. assert.Equal(t, indexer.PSQL, psqlSink.Type())
  130. }
  131. func TestIndexing(t *testing.T) {
  132. ctx, cancel := context.WithCancel(context.Background())
  133. defer cancel()
  134. t.Run("IndexBlockEvents", func(t *testing.T) {
  135. indexer := &EventSink{store: testDB(), chainID: chainID}
  136. require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
  137. verifyBlock(t, 1)
  138. verifyBlock(t, 2)
  139. verifyNotImplemented(t, "hasBlock", func() (bool, error) { return indexer.HasBlock(1) })
  140. verifyNotImplemented(t, "hasBlock", func() (bool, error) { return indexer.HasBlock(2) })
  141. verifyNotImplemented(t, "block search", func() (bool, error) {
  142. v, err := indexer.SearchBlockEvents(ctx, nil)
  143. return v != nil, err
  144. })
  145. require.NoError(t, verifyTimeStamp(tableBlocks))
  146. // Attempting to reindex the same events should gracefully succeed.
  147. require.NoError(t, indexer.IndexBlockEvents(newTestBlockHeader()))
  148. })
  149. t.Run("IndexTxEvents", func(t *testing.T) {
  150. indexer := &EventSink{store: testDB(), chainID: chainID}
  151. txResult := txResultWithEvents([]abci.Event{
  152. makeIndexedEvent("account.number", "1"),
  153. makeIndexedEvent("account.owner", "Ivan"),
  154. makeIndexedEvent("account.owner", "Yulieta"),
  155. {Type: "", Attributes: []abci.EventAttribute{{Key: "not_allowed", Value: "Vlad", Index: true}}},
  156. })
  157. require.NoError(t, indexer.IndexTxEvents([]*abci.TxResult{txResult}))
  158. txr, err := loadTxResult(types.Tx(txResult.Tx).Hash())
  159. require.NoError(t, err)
  160. assert.Equal(t, txResult, txr)
  161. require.NoError(t, verifyTimeStamp(tableTxResults))
  162. require.NoError(t, verifyTimeStamp(viewTxEvents))
  163. verifyNotImplemented(t, "getTxByHash", func() (bool, error) {
  164. txr, err := indexer.GetTxByHash(types.Tx(txResult.Tx).Hash())
  165. return txr != nil, err
  166. })
  167. verifyNotImplemented(t, "tx search", func() (bool, error) {
  168. txr, err := indexer.SearchTxEvents(ctx, nil)
  169. return txr != nil, err
  170. })
  171. // try to insert the duplicate tx events.
  172. err = indexer.IndexTxEvents([]*abci.TxResult{txResult})
  173. require.NoError(t, err)
  174. })
  175. }
  176. func TestStop(t *testing.T) {
  177. indexer := &EventSink{store: testDB()}
  178. require.NoError(t, indexer.Stop())
  179. }
  180. // newTestBlockHeader constructs a fresh copy of a block header containing
  181. // known test values to exercise the indexer.
  182. func newTestBlockHeader() types.EventDataNewBlockHeader {
  183. return types.EventDataNewBlockHeader{
  184. Header: types.Header{Height: 1},
  185. ResultFinalizeBlock: abci.ResponseFinalizeBlock{
  186. BlockEvents: []abci.Event{
  187. makeIndexedEvent("finalize_event.proposer", "FCAA001"),
  188. makeIndexedEvent("thingy.whatzit", "O.O"),
  189. makeIndexedEvent("my_event.foo", "100"),
  190. makeIndexedEvent("thingy.whatzit", "-.O"),
  191. },
  192. },
  193. }
  194. }
  195. // readSchema loads the indexing database schema file
  196. func readSchema() ([]*schema.Migration, error) {
  197. const filename = "schema.sql"
  198. contents, err := os.ReadFile(filename)
  199. if err != nil {
  200. return nil, fmt.Errorf("failed to read sql file from '%s': %w", filename, err)
  201. }
  202. return []*schema.Migration{{
  203. ID: time.Now().Local().String() + " db schema",
  204. Script: string(contents),
  205. }}, nil
  206. }
  207. // resetDB drops all the data from the test database.
  208. func resetDatabase(db *sql.DB) error {
  209. _, err := db.Exec(`DROP TABLE IF EXISTS blocks,tx_results,events,attributes CASCADE;`)
  210. if err != nil {
  211. return fmt.Errorf("dropping tables: %w", err)
  212. }
  213. _, err = db.Exec(`DROP VIEW IF EXISTS event_attributes,block_events,tx_events CASCADE;`)
  214. if err != nil {
  215. return fmt.Errorf("dropping views: %w", err)
  216. }
  217. return nil
  218. }
  219. // txResultWithEvents constructs a fresh transaction result with fixed values
  220. // for testing, that includes the specified events.
  221. func txResultWithEvents(events []abci.Event) *abci.TxResult {
  222. return &abci.TxResult{
  223. Height: 1,
  224. Index: 0,
  225. Tx: types.Tx("HELLO WORLD"),
  226. Result: abci.ExecTxResult{
  227. Data: []byte{0},
  228. Code: abci.CodeTypeOK,
  229. Log: "",
  230. TxEvents: events,
  231. },
  232. }
  233. }
  234. func loadTxResult(hash []byte) (*abci.TxResult, error) {
  235. hashString := fmt.Sprintf("%X", hash)
  236. var resultData []byte
  237. if err := testDB().QueryRow(`
  238. SELECT tx_result FROM `+tableTxResults+` WHERE tx_hash = $1;
  239. `, hashString).Scan(&resultData); err != nil {
  240. return nil, fmt.Errorf("lookup transaction for hash %q failed: %v", hashString, err)
  241. }
  242. txr := new(abci.TxResult)
  243. if err := proto.Unmarshal(resultData, txr); err != nil {
  244. return nil, fmt.Errorf("unmarshaling txr: %w", err)
  245. }
  246. return txr, nil
  247. }
  248. func verifyTimeStamp(tableName string) error {
  249. return testDB().QueryRow(fmt.Sprintf(`
  250. SELECT DISTINCT %[1]s.created_at
  251. FROM %[1]s
  252. WHERE %[1]s.created_at >= $1;
  253. `, tableName), time.Now().Add(-2*time.Second)).Err()
  254. }
  255. func verifyBlock(t *testing.T, height int64) {
  256. // Check that the blocks table contains an entry for this height.
  257. if err := testDB().QueryRow(`
  258. SELECT height FROM `+tableBlocks+` WHERE height = $1;
  259. `, height).Err(); err == sql.ErrNoRows {
  260. t.Errorf("No block found for height=%d", height)
  261. } else if err != nil {
  262. t.Fatalf("Database query failed: %v", err)
  263. }
  264. // Verify the presence of begin_block and end_block events.
  265. if err := testDB().QueryRow(`
  266. SELECT type, height, chain_id FROM `+viewBlockEvents+`
  267. WHERE height = $1 AND type = $2 AND chain_id = $3;
  268. `, height, types.EventTypeBeginBlock, chainID).Err(); err == sql.ErrNoRows {
  269. t.Errorf("No %q event found for height=%d", types.EventTypeBeginBlock, height)
  270. } else if err != nil {
  271. t.Fatalf("Database query failed: %c", err)
  272. }
  273. if err := testDB().QueryRow(`
  274. SELECT type, height, chain_id FROM `+viewBlockEvents+`
  275. WHERE height = $1 AND type = $2 AND chain_id = $3;
  276. `, height, types.EventTypeEndBlock, chainID).Err(); err == sql.ErrNoRows {
  277. t.Errorf("No %q event found for height=%d", types.EventTypeEndBlock, height)
  278. } else if err != nil {
  279. t.Fatalf("Database query failed: %v", err)
  280. }
  281. }
  282. // verifyNotImplemented calls f and verifies that it returns both a
  283. // false-valued flag and a non-nil error whose string matching the expected
  284. // "not supported" message with label prefixed.
  285. func verifyNotImplemented(t *testing.T, label string, f func() (bool, error)) {
  286. t.Helper()
  287. t.Logf("Verifying that %q reports it is not implemented", label)
  288. want := label + " is not supported via the postgres event sink"
  289. ok, err := f()
  290. assert.False(t, ok)
  291. require.Error(t, err)
  292. assert.Equal(t, want, err.Error())
  293. }
  294. // waitForInterrupt blocks until a SIGINT is received by the process.
  295. func waitForInterrupt() {
  296. ch := make(chan os.Signal, 1)
  297. signal.Notify(ch, os.Interrupt)
  298. <-ch
  299. }