package postgres import ( "context" "database/sql" "encoding/hex" "encoding/json" "fmt" "testing" "testing/fstest" "time" "github.com/DATA-DOG/go-sqlmock" abci "github.com/cometbft/cometbft/abci/types" ctypes "github.com/cometbft/cometbft/rpc/core/types" "github.com/lib/pq" "github.com/stretchr/testify/require" "github.com/ignite/cli/v29/ignite/pkg/cosmosclient" "github.com/ignite/cli/v29/ignite/pkg/cosmostxcollector/query" "github.com/ignite/cli/v29/ignite/pkg/errors" ) var ( eventFields = []string{"id", "index", "tx_hash", "type", "created_at"} eventAttrFields = []string{"event_id", "name", "value"} ) func TestUpdateSchema(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() ctx := context.Background() tplSchemaScript := `BEGIN; INSERT INTO schema(version) VALUES(%d) ;%sCOMMIT;` // Arrange: Schema files schemasData := []string{"/* FOO */", "/* BAR */"} fs := fstest.MapFS{ "schemas/1.sql": &fstest.MapFile{Data: []byte(schemasData[0])}, "schemas/2.sql": &fstest.MapFile{Data: []byte(schemasData[1])}, } s := NewSchemas(fs, "") // Arrange: Prepare database adapter adapter := Adapter{ db: db, schemas: s, } // Arrange: Database mock and expectations mock. ExpectExec(s.GetTableDDL()). WillReturnResult( // DDL execution won't affect any rows or IDs sqlmock.NewResult(0, 0), ) mock. ExpectQuery(s.GetSchemaVersionSQL()). WillReturnRows( // Zero is returned to signal that there are no versions applied. // When no versions are applied schema walk will start from version 1. sqlmock.NewRows([]string{"version"}).AddRow(uint64(0)), ) for i, data := range schemasData { version := i + 1 script := fmt.Sprintf(tplSchemaScript, version, data) // Add database mock and expectation for the current schema version mock. ExpectExec(script). WillReturnResult(sqlmock.NewResult(0, 0)) } // Act err := adapter.UpdateSchema(ctx, s) // Assert require.NoError(t, err) require.NoError(t, mock.ExpectationsWereMet()) } func TestSave(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() hash := "F2564C78071E26643AE9B3E2A19FA0DC10D4D9E873AA0BE808660123F11A1E78" // Arrange: A Cosmos client TX to save evtAttr := abci.EventAttribute{ Key: "recipient", Value: "cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5", } evt := abci.Event{ Type: "transfer", Attributes: []abci.EventAttribute{evtAttr}, } h, _ := hex.DecodeString(hash) // TODO: How to properly generate TX hash for the result? tx := cosmosclient.TX{ // Tendermint API search result Raw: &ctypes.ResultTx{ Hash: h, Height: 1, Index: 0, TxResult: abci.ExecTxResult{ Events: []abci.Event{evt}, }, }, } // Arrange: JSON of the raw transaction result jsonResTX, err := json.Marshal(tx.Raw) require.NoError(t, err) // Arrange: Database mock and expectations for prepared SQL statements mock.ExpectBegin() txStmt := mock.ExpectPrepare(` INSERT INTO tx (hash, index, height, block_time) VALUES ($1, $2, $3, $4) `) evtStmt := mock.ExpectPrepare(` INSERT INTO event (tx_hash, type, index) VALUES ($1, $2, $3) RETURNING id `) attrStmt := mock.ExpectPrepare(` INSERT INTO attribute (event_id, name, value) VALUES ($1, $2, $3) `) // Arrange: Database mock and expectations for INSERT statement executions insertResult := sqlmock.NewResult(0, 1) evtIndex := 0 evtID := int64(1) jsonEvtAttrValue := []byte(fmt.Sprintf(`"%s"`, evtAttr.Value)) mock. ExpectExec(` INSERT INTO raw_tx (hash, data) VALUES ($1, $2) `). WithArgs(tx.Raw.Hash.String(), jsonResTX). WillReturnResult(insertResult) txStmt. ExpectExec(). WithArgs(hash, tx.Raw.Index, tx.Raw.Height, tx.BlockTime). WillReturnResult(insertResult) evtStmt. ExpectQuery(). WithArgs(hash, evt.Type, evtIndex). WillReturnRows( sqlmock.NewRows([]string{"event_id"}).AddRow(evtID), ) attrStmt. ExpectExec(). WithArgs(evtID, evtAttr.Key, jsonEvtAttrValue). WillReturnResult(insertResult) mock.ExpectCommit() // Act err = adapter.Save(ctx, []cosmosclient.TX{tx}) // Assert require.NoError(t, err) require.NoError(t, mock.ExpectationsWereMet()) } func TestGetLatestHeight(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() // Arrange: Database mock and expectations wantHeight := int64(42) mock. ExpectQuery(sqlSelectBlockHeight). WillReturnRows( sqlmock.NewRows([]string{"height"}).AddRow(wantHeight), ) // Act height, err := adapter.GetLatestHeight(ctx) // Assert require.NoError(t, err) require.Equal(t, wantHeight, height) require.NoError(t, mock.ExpectationsWereMet()) } func TestQuery(t *testing.T) { // Arrange var rowValue string db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() // Arrange: Query qry := query.New("baz", query.Fields("foo")) // Arrange: Database mock and expectations wantRowValue := "expected" fields := []string{"foo"} rows := sqlmock.NewRows(fields).AddRow(wantRowValue) mock. ExpectQuery(` SELECT DISTINCT foo FROM baz WHERE true LIMIT 30 OFFSET 0 `). WillReturnRows(rows) // Act cr, err := adapter.Query(ctx, qry) if cr.Next() { err = cr.Scan(&rowValue) require.NoError(t, err) } // Assert require.NoError(t, err, "expected no query errors on execution") require.Equal(t, wantRowValue, rowValue) require.NoError(t, mock.ExpectationsWereMet()) } func TestQueryCursor(t *testing.T) { // Arrange var ( rowValue string cursorNextSucceeded bool err error ) db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() // Arrange: Query qry := query.New("baz", query.Fields("foo")) // Arrange: Database mock and expectations wantRowValue := "expected" fields := []string{"foo"} rows := sqlmock.NewRows(fields).AddRow(wantRowValue) mock. ExpectQuery(` SELECT DISTINCT foo FROM baz WHERE true LIMIT 30 OFFSET 0 `). WillReturnRows(rows) // Act cr, _ := adapter.Query(ctx, qry) if cursorNextSucceeded = cr.Next(); cursorNextSucceeded { err = cr.Scan(&rowValue) } // Assert require.True(t, cursorNextSucceeded, "expected cursor.Next() to succeed") require.NoError(t, err, "expected no scan errors on execution") require.Equal(t, wantRowValue, rowValue) require.NoError(t, mock.ExpectationsWereMet()) } func TestQueryWithFilter(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() // Arrange: Query wantArg := "bar" qry := query.New( "baz", query.Fields("foo"), query.WithFilters( NewFilter("foo", wantArg), ), ) // Arrange: Database mock and expectations fields := []string{"baz"} rows := sqlmock.NewRows(fields) mock. ExpectQuery(` SELECT DISTINCT foo FROM baz WHERE foo = $1 LIMIT 30 OFFSET 0 `). WithArgs(wantArg). WillReturnRows(rows) // Act _, err := adapter.Query(ctx, qry) // Assert require.NoError(t, err, "expected no query errors on execution") require.NoError(t, mock.ExpectationsWereMet()) } func TestQueryError(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() // Arrange: Query qry := query.New("baz", query.Fields("foo"), query.WithoutPaging()) // Arrange: Database mock and expectations wantErr := errors.New("expected error") mock. ExpectQuery("SELECT DISTINCT foo FROM baz WHERE true"). WillReturnError(wantErr) // Act _, err := adapter.Query(ctx, qry) // Assert require.Equal(t, wantErr, err) require.NoError(t, mock.ExpectationsWereMet()) } func TestQueryRowError(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() cols := []string{"name"} // Arrange: Query qry := query.New("baz", query.Fields(cols[0]), query.WithoutPaging()) // Arrange: Database mock and expectations wantErr := errors.New("expected error") row := sqlmock. NewRows(cols). AddRow("foo"). RowError(0, wantErr) mock. ExpectQuery("SELECT DISTINCT name FROM baz WHERE true"). WillReturnRows(row) // Act cr, err := adapter.Query(ctx, qry) // Assert require.NoError(t, err, "expected no query errors on execution") require.False(t, cr.Next(), "expected cursor.Next() to fail") require.Equal(t, wantErr, cr.Err()) require.NoError(t, mock.ExpectationsWereMet()) } func TestEventQuery(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() // Arrange: Database mocks attrName := "foo" attrValue := []byte("42") event := query.Event{ ID: 1, TXHash: "ABC123", Index: 0, Type: "test", Attributes: []query.Attribute{ query.NewAttribute(attrName, attrValue), }, CreatedAt: time.Now(), } eventRows := sqlmock. NewRows(eventFields). AddRow(event.ID, event.Index, event.TXHash, event.Type, event.CreatedAt) eventAttrRows := sqlmock. NewRows(eventAttrFields). AddRow(event.ID, attrName, attrValue) mock. ExpectQuery(` SELECT event.id, event.index, event.tx_hash, event.type, event.created_at FROM event INNER JOIN tx ON event.tx_hash = tx.hash WHERE true ORDER BY tx.height, tx.index, event.index LIMIT 30 OFFSET 0 `). WillReturnRows(eventRows) mock. ExpectQuery(` SELECT event_id, name, value FROM attribute WHERE event_id = ANY($1) ORDER BY event_id `). WillReturnRows(eventAttrRows). WithArgs(pq.Array([]int64{event.ID})) // Arrange: Expectations wantEvents := []query.Event{event} // Arrange: Query qry := query.NewEventQuery() // Act events, err := adapter.QueryEvents(ctx, qry) // Assert require.NoError(t, err, "expected no query errors on execution") require.Len(t, events, 1) require.Equal(t, wantEvents, events) require.NoError(t, mock.ExpectationsWereMet()) } func TestEventQueryWithFilters(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() // Arrange: Database mocks indexValue := 2 typeValue := "chain.test.Test" hashValues := []string{"HASH1", "HASH2"} eventRows := sqlmock.NewRows(eventFields) mock. ExpectQuery(` SELECT event.id, event.index, event.tx_hash, event.type, event.created_at FROM event INNER JOIN tx ON event.tx_hash = tx.hash WHERE event.index = $1 AND event.type = $2 AND event.tx_hash = ANY($3) ORDER BY tx.height, tx.index, event.index LIMIT 30 OFFSET 0 `). WillReturnRows(eventRows). WithArgs(indexValue, typeValue, pq.Array(hashValues)) // Arrange: Query qry := query.NewEventQuery( query.WithFilters( NewFilter("event.index", indexValue), FilterByEventType(typeValue), FilterByEventTXs(hashValues...), ), ) // Act events, err := adapter.QueryEvents(ctx, qry) // Assert require.NoError(t, err, "expected no query errors on execution") require.Len(t, events, 0) require.NoError(t, mock.ExpectationsWereMet()) } func TestEventQueryWithEventAttrFilters(t *testing.T) { // Arrange db, mock := createMatchEqualSQLMock(t) defer db.Close() adapter := Adapter{db: db} ctx := context.Background() // Arrange: Database mocks attrNameValue := "foo" attrValue := int64(42) eventRows := sqlmock.NewRows(eventFields) mock. ExpectQuery(` SELECT DISTINCT events.* FROM ( SELECT event.id, event.index, event.tx_hash, event.type, event.created_at FROM event INNER JOIN tx ON event.tx_hash = tx.hash INNER JOIN attribute ON event.id = attribute.event_id WHERE attribute.name = $1 AND attribute.name = $2 AND attribute.value::numeric = $3 ORDER BY tx.height, tx.index, event.index ) AS events LIMIT 30 OFFSET 0 `). WillReturnRows(eventRows). WithArgs(attrNameValue, attrNameValue, attrValue) // Arrange: Query qry := query.NewEventQuery( query.WithFilters( NewFilter("attribute.name", attrNameValue), FilterByEventAttrName(attrNameValue), FilterByEventAttrValueInt(attrValue), ), ) // Act events, err := adapter.QueryEvents(ctx, qry) // Assert require.NoError(t, err, "expected no query errors on execution") require.Len(t, events, 0) require.NoError(t, mock.ExpectationsWereMet()) } func createMatchEqualSQLMock(t *testing.T) (*sql.DB, sqlmock.Sqlmock) { t.Helper() db, mock, err := sqlmock.New( sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), ) require.NoError(t, err) return db, mock }