diff --git a/go.mod b/go.mod index 1f8b16cffe..83effd8f09 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/taikoxyz/taiko-mono go 1.21 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/buildkite/terminal-to-html/v3 v3.8.0 github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.3.0 @@ -35,6 +36,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/datatypes v1.2.0 gorm.io/driver/mysql v1.5.6 + gorm.io/driver/sqlite v1.4.3 gorm.io/gorm v1.25.9 gotest.tools v2.2.0+incompatible ) @@ -129,6 +131,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/microcosm-cc/bluemonday v1.0.21 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect @@ -158,6 +161,7 @@ require ( github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/supranational/blst v0.3.11 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect diff --git a/go.sum b/go.sum index 2df1e3f7e6..38c09d02d1 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UT github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw= github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4= github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8= github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= @@ -339,6 +341,7 @@ github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJS github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4= @@ -354,6 +357,7 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.11.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.12/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= @@ -551,6 +555,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -853,6 +859,7 @@ gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.3/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/packages/blobstorage/indexer/indexer.go b/packages/blobstorage/indexer/indexer.go index ba093b4aaa..b56dc03437 100644 --- a/packages/blobstorage/indexer/indexer.go +++ b/packages/blobstorage/indexer/indexer.go @@ -103,9 +103,7 @@ func (i *Indexer) setInitialIndexingBlock( ctx context.Context, ) error { // get most recently processed block height from the DB - // latest, err := i.blobHashRepo.FindLatestBlockID() latest, err := i.repositories.BlockMetaRepo.FindLatestBlockID() - // latest, err := i.blockMetaRepo.FindLatestBlockID() if err != nil { return err } @@ -271,7 +269,6 @@ func calculateBlobHash(commitmentStr string) common.Hash { } func (i *Indexer) checkReorg(ctx context.Context, event *taikol1.TaikoL1BlockProposed) error { - // n, err := i.blockMetaRepo.FindLatestBlockID() n, err := i.repositories.BlockMetaRepo.FindLatestBlockID() if err != nil { return err diff --git a/packages/blobstorage/pkg/mocks/blob_hash.go b/packages/blobstorage/pkg/mocks/blob_hash.go new file mode 100644 index 0000000000..169b2865ee --- /dev/null +++ b/packages/blobstorage/pkg/mocks/blob_hash.go @@ -0,0 +1,26 @@ +package mocks + +import ( + "github.com/stretchr/testify/mock" + "github.com/taikoxyz/taiko-mono/packages/blobstorage" +) + +type MockBlobHashRepo struct { + mock.Mock +} + +func (m *MockBlobHashRepo) FirstByBlobHash(blobHash string) (*blobstorage.BlobHash, error) { + args := m.Called(blobHash) + if item, ok := args.Get(0).(*blobstorage.BlobHash); ok { + return item, args.Error(1) + } + return nil, args.Error(1) +} + +func (m *MockBlobHashRepo) Save(opts blobstorage.SaveBlobHashOpts) error { + return m.Called(opts).Error(0) +} + +func (m *MockBlobHashRepo) DeleteAllAfterBlockID(blockID uint64) error { + return m.Called(blockID).Error(0) +} diff --git a/packages/blobstorage/pkg/mocks/block_meta.go b/packages/blobstorage/pkg/mocks/block_meta.go new file mode 100644 index 0000000000..1a4159997a --- /dev/null +++ b/packages/blobstorage/pkg/mocks/block_meta.go @@ -0,0 +1,23 @@ +package mocks + +import ( + "github.com/stretchr/testify/mock" + blobstorage "github.com/taikoxyz/taiko-mono/packages/blobstorage" +) + +type MockBlockMetaRepo struct { + mock.Mock +} + +func (m *MockBlockMetaRepo) Save(opts blobstorage.SaveBlockMetaOpts) error { + return m.Called(opts).Error(0) +} + +func (m *MockBlockMetaRepo) FindLatestBlockID() (uint64, error) { + args := m.Called() + return args.Get(0).(uint64), args.Error(1) +} + +func (m *MockBlockMetaRepo) DeleteAllAfterBlockID(blockID uint64) error { + return m.Called(blockID).Error(0) +} diff --git a/packages/blobstorage/pkg/mocks/db.go b/packages/blobstorage/pkg/mocks/db.go new file mode 100644 index 0000000000..656f545349 --- /dev/null +++ b/packages/blobstorage/pkg/mocks/db.go @@ -0,0 +1,62 @@ +package mocks + +import ( + "database/sql" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/mock" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type MockDB struct { + mock.Mock + gormDB *gorm.DB +} + +func (m *MockDB) DB() (*sql.DB, error) { + args := m.Called() + return args.Get(0).(*sql.DB), args.Error(1) +} + +func (m *MockDB) GormDB() *gorm.DB { + args := m.Called() + return args.Get(0).(*gorm.DB) +} + +func (db *MockDB) Begin() *gorm.DB { + args := db.Called() + return args.Get(0).(*gorm.DB) +} + +func (db *MockDB) Commit() error { + args := db.Called() + return args.Error(0) +} + +func (db *MockDB) Rollback() error { + args := db.Called() + return args.Error(0) +} + +func NewMockDB(t *testing.T) (*MockDB, sqlmock.Sqlmock, *gorm.DB, func()) { + sqlDB, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("newMockDB: %v", err) + } + + mock.ExpectQuery("select sqlite_version()"). + WillReturnRows(sqlmock.NewRows([]string{"version"}). + AddRow("3.31.1")) + + gormDB, err := gorm.Open(sqlite.Dialector{Conn: sqlDB}, &gorm.Config{}) + if err != nil { + t.Fatalf("newMockDB: %v", err) + } + + mockDB := &MockDB{gormDB: gormDB} + closeDB := func() { sqlDB.Close() } + + return mockDB, mock, gormDB, closeDB +} diff --git a/packages/blobstorage/pkg/repo/blob_hash.go b/packages/blobstorage/pkg/repo/blob_hash.go index 125f643ee6..0f78908b99 100644 --- a/packages/blobstorage/pkg/repo/blob_hash.go +++ b/packages/blobstorage/pkg/repo/blob_hash.go @@ -23,6 +23,7 @@ func (r *BlobHashRepository) startQuery() *gorm.DB { return r.db.GormDB().Table("blob_hashes") } +/* Save a blob_hash record */ func (r *BlobHashRepository) Save(opts blobstorage.SaveBlobHashOpts) error { b := &blobstorage.BlobHash{ BlobHash: opts.BlobHash, @@ -36,6 +37,7 @@ func (r *BlobHashRepository) Save(opts blobstorage.SaveBlobHashOpts) error { return nil } +/* Get a block_hash record by a blobHash */ func (r *BlobHashRepository) FirstByBlobHash(blobHash string) (*blobstorage.BlobHash, error) { var b blobstorage.BlobHash @@ -46,7 +48,7 @@ func (r *BlobHashRepository) FirstByBlobHash(blobHash string) (*blobstorage.Blob return &b, nil } -// DeleteAllAfterBlockID is used when a reorg is detected +/* Delete all records from block_hash tables starting from a blockID */ func (r *BlobHashRepository) DeleteAllAfterBlockID(blockID uint64) error { query := ` DELETE FROM blob_hashes diff --git a/packages/blobstorage/pkg/repo/blob_hash_test.go b/packages/blobstorage/pkg/repo/blob_hash_test.go new file mode 100644 index 0000000000..1784a679f8 --- /dev/null +++ b/packages/blobstorage/pkg/repo/blob_hash_test.go @@ -0,0 +1,210 @@ +package repo + +import ( + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + blobstorage "github.com/taikoxyz/taiko-mono/packages/blobstorage" + "github.com/taikoxyz/taiko-mono/packages/blobstorage/pkg/mocks" + "gorm.io/gorm" +) + +func TestBlobHashRepository_Save(t *testing.T) { + testCases := []struct { + name string + opts blobstorage.SaveBlobHashOpts + setupMock func(mock sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "successful to save a blob_hash", + opts: blobstorage.SaveBlobHashOpts{ + BlobHash: "hash123", + KzgCommitment: "commitment123", + BlobData: "data123", + }, + setupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `blob_hashes`"). + WithArgs("hash123", "commitment123", "data123"). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + { + name: "failed to save a blob_hash", + opts: blobstorage.SaveBlobHashOpts{ + BlobHash: "hash123", + KzgCommitment: "commitment123", + BlobData: "data123", + }, + setupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `blob_hashes`"). + WithArgs("hash123", "commitment123", "data123"). + WillReturnError(gorm.ErrInvalidDB) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB, mock, gormDB, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockDB.On("GormDB").Return(gormDB) + + repository, err := NewBlobHashRepository(mockDB) + if err != nil { + t.Fatalf("TestBlobHashRepo_Save : %s", err) + } + + tc.setupMock(mock) + + err = repository.Save(tc.opts) + if (err != nil) != tc.wantErr { + t.Errorf("TestBlobHashRepo_Save: expected error %v, got %v", tc.wantErr, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("TestBlobHashRepo_Save : %s", err) + } + mockDB.AssertExpectations(t) + }) + } +} + +func TestBlobHashRepo_FirstByBlobHash(t *testing.T) { + testCases := []struct { + name string + blobHash string + setupMock func(mock sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "successful to get a blob_hash by blobHash", + blobHash: "hash123", + setupMock: func(mock sqlmock.Sqlmock) { + expected_rows := sqlmock.NewRows( + []string{"blob_hash", + "kzg_commitment", + "blob_data"}). + AddRow("hash123", "commitment123", "data123") + + query := "SELECT * FROM `blob_hashes` WHERE blob_hash = ? ORDER BY `blob_hashes`.`blob_hash` LIMIT 1" + mock.ExpectQuery(regexp.QuoteMeta(query)). + WithArgs("hash123"). + WillReturnRows(expected_rows) + + }, + wantErr: false, + }, + { + name: "failed to get a blob_hash by blobHash", + blobHash: "hash123", + setupMock: func(mock sqlmock.Sqlmock) { + query := "SELECT * FROM `blob_hashes` WHERE blob_hash = ? ORDER BY `blob_hashes`.`blob_hash` LIMIT 1" + mock.ExpectQuery(regexp.QuoteMeta(query)). + WithArgs("hash123"). + WillReturnError(gorm.ErrInvalidDB) + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB, mock, gormDB, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockDB.On("GormDB").Return(gormDB) + + repository, err := NewBlobHashRepository(mockDB) + if err != nil { + t.Fatalf("TestBlobHashRepo_FirstByBlobHash : %s", err) + } + + tc.setupMock(mock) + + _, err = repository.FirstByBlobHash(tc.blobHash) + if (err != nil) != tc.wantErr { + t.Errorf("TestBlobHashRepo_FirstByBlobHash: expected error %v, got %v", tc.wantErr, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("TestBlobHashRepo_FirstByBlobHash : %s", err) + } + mockDB.AssertExpectations(t) + }) + } +} + +func TestBlobHashRepo_DeleteAllAfterBlockID(t *testing.T) { + testCases := []struct { + name string + blockID uint64 + setupMock func(mock sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "successful to delete a blob_hashes starting from a blockID", + blockID: 123, + setupMock: func(mock sqlmock.Sqlmock) { + query := `DELETE FROM blob_hashes WHERE blob_hash IN \(\s*SELECT blob_hashes\.blob_hash FROM blob_hashes INNER JOIN block_meta ON blob_hashes\.blob_hash = block_meta\.blob_hash WHERE block_meta\.block_id >= \?\s*\)` + mock.ExpectExec(query). + WithArgs(123). + WillReturnResult(sqlmock.NewResult(0, 1)) + }, + wantErr: false, + }, + { + name: "delete to to delete a blob_hashes starting from a blockID", + blockID: 123, + setupMock: func(mock sqlmock.Sqlmock) { + query := `DELETE FROM blob_hashes WHERE blob_hash IN \(\s*SELECT blob_hashes\.blob_hash FROM blob_hashes INNER JOIN block_meta ON blob_hashes\.blob_hash = block_meta\.blob_hash WHERE block_meta\.block_id >= \?\s*\)` + mock.ExpectExec(query). + WithArgs(123). + WillReturnError(gorm.ErrInvalidDB) + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB, mock, gormDB, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockDB.On("GormDB").Return(gormDB) + + repository, err := NewBlobHashRepository(mockDB) + if err != nil { + t.Fatalf("TestBlobHashRepo_DeleteAllAfterBlockID : %s", err) + } + + tc.setupMock(mock) + + err = repository.DeleteAllAfterBlockID(tc.blockID) + if (err != nil) != tc.wantErr { + t.Errorf("TestBlobHashRepo_DeleteAllAfterBlockID: expected error %v, got %v", tc.wantErr, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("TestBlobHashRepo_DeleteAllAfterBlockID : %s", err) + } + mockDB.AssertExpectations(t) + }) + } +} + +func TestNewBlobHash_InvalidDB(t *testing.T) { + repo, err := NewBlobHashRepository(nil) + assert.Nil(t, repo) + assert.Error(t, err) + assert.Equal(t, ErrNoDB, err) +} diff --git a/packages/blobstorage/pkg/repo/block_meta.go b/packages/blobstorage/pkg/repo/block_meta.go index c5e886ad99..a51b30c5f7 100644 --- a/packages/blobstorage/pkg/repo/block_meta.go +++ b/packages/blobstorage/pkg/repo/block_meta.go @@ -37,8 +37,7 @@ func (r *BlockMetaRepository) Save(opts blobstorage.SaveBlockMetaOpts) error { } func (r *BlockMetaRepository) FindLatestBlockID() (uint64, error) { - q := `SELECT COALESCE(MAX(emitted_block_id), 0) - FROM blocks_meta` + q := `SELECT COALESCE(MAX(emitted_block_id), 0) FROM blocks_meta` var b uint64 @@ -51,9 +50,7 @@ func (r *BlockMetaRepository) FindLatestBlockID() (uint64, error) { // DeleteAllAfterBlockID is used when a reorg is detected func (r *BlockMetaRepository) DeleteAllAfterBlockID(blockID uint64) error { - query := ` -DELETE FROM blob_hashes -WHERE block_id >= ?` + query := `DELETE FROM blob_hashes WHERE block_id >= ?` if err := r.startQuery().Exec(query, blockID).Error; err != nil { return err diff --git a/packages/blobstorage/pkg/repo/block_meta_test.go b/packages/blobstorage/pkg/repo/block_meta_test.go new file mode 100644 index 0000000000..eda96745c5 --- /dev/null +++ b/packages/blobstorage/pkg/repo/block_meta_test.go @@ -0,0 +1,203 @@ +package repo + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/taikoxyz/taiko-mono/packages/blobstorage" + "github.com/taikoxyz/taiko-mono/packages/blobstorage/pkg/mocks" + "gorm.io/gorm" +) + +func TestBlockMetaRepo_Save(t *testing.T) { + testCases := []struct { + name string + opts blobstorage.SaveBlockMetaOpts + setupMock func(mock sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "successful to save a block_meta", + opts: blobstorage.SaveBlockMetaOpts{ + BlobHash: "hash123", + BlockID: 1, + EmittedBlockID: 2, + }, + setupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `blocks_meta`"). + WithArgs("hash123", 1, 2). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + { + name: "failed to save a block_meta", + opts: blobstorage.SaveBlockMetaOpts{ + BlobHash: "hash123", + BlockID: 1, + EmittedBlockID: 2, + }, + setupMock: func(mock sqlmock.Sqlmock) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `blocks_meta`"). + WithArgs("hash123", 1, 2). + WillReturnError(gorm.ErrInvalidDB) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB, mock, gormDB, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockDB.On("GormDB").Return(gormDB) + + repository, err := NewBlockMetaRepository(mockDB) + if err != nil { + t.Fatalf("TestBlobHashRepo_Save : %s", err) + } + + tc.setupMock(mock) + + err = repository.Save(tc.opts) + if (err != nil) != tc.wantErr { + t.Errorf("TestBlobHashRepo_Save: expected error %v, got %v", tc.wantErr, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("TestBlobHashRepo_Save : %s", err) + } + mockDB.AssertExpectations(t) + }) + } +} + +func TestBlockMetaRepo_FindLatestBlockID(t *testing.T) { + testCases := []struct { + name string + mockSetup func(mock sqlmock.Sqlmock) + expected uint64 + wantErr bool + }{ + { + name: "successfully find latest block ID", + mockSetup: func(mock sqlmock.Sqlmock) { + query := `SELECT COALESCE\(MAX\(emitted_block_id\), 0\) FROM blocks_meta` + expected_rows := sqlmock.NewRows([]string{"emitted_block_id"}).AddRow(5) + mock.ExpectQuery(query). + WillReturnRows(expected_rows) + }, + expected: 5, + wantErr: false, + }, + { + name: "error finding latest block ID", + mockSetup: func(mock sqlmock.Sqlmock) { + query := `SELECT COALESCE\(MAX\(emitted_block_id\), 0\) FROM blocks_meta` + mock.ExpectQuery(query). + WillReturnError(gorm.ErrInvalidDB) + }, + expected: 0, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB, mock, gormDB, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockDB.On("GormDB").Return(gormDB) + + repository, err := NewBlockMetaRepository(mockDB) + if err != nil { + t.Fatalf("TestBlockMetaRepo_FindLatestBlockID : %s", err) + } + + tc.mockSetup(mock) + + result, err := repository.FindLatestBlockID() + if (err != nil) != tc.wantErr { + t.Errorf("TestBlockMetaRepo_FindLatestBlockID: expected error %v, got %v", tc.wantErr, err) + } + + if result != tc.expected { + t.Errorf("TestBlockMetaRepo_FindLatestBlockID : %s", err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("TestBlockMetaRepo_FindLatestBlockID: %s", err) + } + }) + } +} + +func TestBlockMetaRepository_DeleteAllAfterBlockID(t *testing.T) { + testCases := []struct { + name string + blockID uint64 + mockSetup func(mock sqlmock.Sqlmock) + wantErr bool + }{ + { + name: "successfully delete after block ID", + blockID: 10, + mockSetup: func(mock sqlmock.Sqlmock) { + query := `DELETE FROM blob_hashes WHERE block_id >= \?` + mock.ExpectExec(query). + WithArgs(10). + WillReturnResult(sqlmock.NewResult(0, 3)) + }, + wantErr: false, + }, + { + name: "failed to delete after block ID", + blockID: 10, + mockSetup: func(mock sqlmock.Sqlmock) { + query := `DELETE FROM blob_hashes WHERE block_id >= \?` + mock.ExpectExec(query). + WithArgs(10). + WillReturnError(gorm.ErrInvalidDB) + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mockDB, mock, gormDB, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockDB.On("GormDB").Return(gormDB) + + repository, err := NewBlockMetaRepository(mockDB) + if err != nil { + t.Fatalf("TestBlockMetaRepo_FindLatestBlockID : %s", err) + } + + tc.mockSetup(mock) + + err = repository.DeleteAllAfterBlockID(tc.blockID) + if (err != nil) != tc.wantErr { + t.Errorf("TestBlockMetaRepo_FindLatestBlockID: expected error %v, got %v", tc.wantErr, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("TestBlockMetaRepo_FindLatestBlockID : %s", err) + } + }) + } +} + +func TestNewBlockMetaRepository_InvalidDB(t *testing.T) { + repo, err := NewBlockMetaRepository(nil) + assert.Nil(t, repo) + assert.Error(t, err) + assert.Equal(t, ErrNoDB, err) +} diff --git a/packages/blobstorage/pkg/repo/db.go b/packages/blobstorage/pkg/repo/db.go index 1c8a35dbca..1bf7772582 100644 --- a/packages/blobstorage/pkg/repo/db.go +++ b/packages/blobstorage/pkg/repo/db.go @@ -4,6 +4,7 @@ import ( "database/sql" "github.com/cyberhorsey/errors" + "gorm.io/gorm" ) @@ -14,4 +15,5 @@ var ( type DB interface { DB() (*sql.DB, error) GormDB() *gorm.DB + // Begin() *gorm.DB } diff --git a/packages/blobstorage/pkg/repo/repositories.go b/packages/blobstorage/pkg/repo/repositories.go index 876160db8b..af10d8dfa0 100644 --- a/packages/blobstorage/pkg/repo/repositories.go +++ b/packages/blobstorage/pkg/repo/repositories.go @@ -8,13 +8,9 @@ import ( "gorm.io/gorm" ) -type DBTransaction interface { - Begin() *DB -} - type Repositories struct { - BlobHashRepo *BlobHashRepository - BlockMetaRepo *BlockMetaRepository + BlobHashRepo blobstorage.BlobHashRepository + BlockMetaRepo blobstorage.BlockMetaRepository dbTransaction DB } @@ -40,7 +36,6 @@ func NewRepositories(db DB) (*Repositories, error) { } // Database transaction to store blobs and blocks meta -// func (r *Repositories) SaveBlobAndBlockMeta(ctx context.Context, opts *blobstorage.SaveBlobAndBlockMetaOpts) error { func (r *Repositories) SaveBlobAndBlockMeta( ctx context.Context, saveBlockMetaOpts *blobstorage.SaveBlockMetaOpts, diff --git a/packages/blobstorage/pkg/repo/repositories_test.go b/packages/blobstorage/pkg/repo/repositories_test.go new file mode 100644 index 0000000000..dcb5b074ae --- /dev/null +++ b/packages/blobstorage/pkg/repo/repositories_test.go @@ -0,0 +1,130 @@ +package repo + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/taikoxyz/taiko-mono/packages/blobstorage" + + "github.com/taikoxyz/taiko-mono/packages/blobstorage/pkg/mocks" + "gorm.io/gorm" +) + +func TestNewRepositories(t *testing.T) { + mockDB, _, _, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockBlobHashRepo := new(mocks.MockBlobHashRepo) + mockBlockMetaRepo := new(mocks.MockBlockMetaRepo) + + repos := &Repositories{ + BlobHashRepo: mockBlobHashRepo, + BlockMetaRepo: mockBlockMetaRepo, + dbTransaction: mockDB, + } + + repos, err := NewRepositories(mockDB) + assert.NoError(t, err) + assert.NotNil(t, repos) +} + +func TestSaveBlobAndBlockMeta(t *testing.T) { + mockDB, mockSql, gormDB, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockDB.On("GormDB").Return(gormDB) + mockSql.ExpectBegin() + mockSql.ExpectCommit() + + mockBlobHashRepo := new(mocks.MockBlobHashRepo) + mockBlockMetaRepo := new(mocks.MockBlockMetaRepo) + + repos := &Repositories{ + BlobHashRepo: mockBlobHashRepo, + BlockMetaRepo: mockBlockMetaRepo, + dbTransaction: mockDB, + } + + ctx := context.Background() + + blobHash := "somehash" + mockBlobHashRepo.On("FirstByBlobHash", blobHash). + Return(nil, gorm.ErrRecordNotFound). + Once() + + mockBlobHashRepo.On("Save", mock.IsType(blobstorage.SaveBlobHashOpts{})). + Return(nil). + Once() + + mockBlockMetaRepo.On("Save", mock.IsType(blobstorage.SaveBlockMetaOpts{})). + Return(nil). + Once() + + saveBlobHashOpts := &blobstorage.SaveBlobHashOpts{ + BlobHash: blobHash, + KzgCommitment: "somecommitment", + BlobData: "somedata", + } + + saveBlockMetaOpts := &blobstorage.SaveBlockMetaOpts{ + BlobHash: blobHash, + BlockID: 123, + EmittedBlockID: 456, + } + + err := repos.SaveBlobAndBlockMeta(ctx, saveBlockMetaOpts, saveBlobHashOpts) + assert.NoError(t, err) + mockBlobHashRepo.AssertExpectations(t) + mockBlockMetaRepo.AssertExpectations(t) + mockDB.AssertExpectations(t) +} + +func TestDeleteAllAfterBlockID(t *testing.T) { + mockDB, mockSql, gormDB, closeDB := mocks.NewMockDB(t) + defer closeDB() + + mockDB.On("GormDB").Return(gormDB) + mockSql.ExpectBegin() + mockSql.ExpectCommit() + mockSql.ExpectRollback() + + mockBlobHashRepo := new(mocks.MockBlobHashRepo) + mockBlockMetaRepo := new(mocks.MockBlockMetaRepo) + + repos := &Repositories{ + BlobHashRepo: mockBlobHashRepo, + BlockMetaRepo: mockBlockMetaRepo, + dbTransaction: mockDB, + } + + ctx := context.Background() + blockID := uint64(123) + + mockBlobHashRepo.On("DeleteAllAfterBlockID", blockID). + Return(nil). + Once() + + mockBlockMetaRepo.On("DeleteAllAfterBlockID", blockID). + Return(nil). + Once() + + err := repos.DeleteAllAfterBlockID(ctx, blockID) + + assert.NoError(t, err) + mockBlobHashRepo.AssertExpectations(t) + mockBlockMetaRepo.AssertExpectations(t) + mockDB.AssertExpectations(t) + + mockSql.ExpectBegin() + mockSql.ExpectRollback() + mockBlobHashRepo.On("DeleteAllAfterBlockID", blockID). + Return(gorm.ErrRecordNotFound). + Once() + + err = repos.DeleteAllAfterBlockID(ctx, blockID) + assert.ErrorIs(t, err, gorm.ErrRecordNotFound) + + mockDB.AssertExpectations(t) +}