From c092ba615b2e3be62a1a791157ead85059bd720c Mon Sep 17 00:00:00 2001 From: itsLeonB Date: Tue, 30 Dec 2025 18:36:17 +0700 Subject: [PATCH 1/6] feat: remove deleted at --- base_entity.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/base_entity.go b/base_entity.go index 45fc81a..bd90cb6 100644 --- a/base_entity.go +++ b/base_entity.go @@ -1,23 +1,17 @@ package crud import ( - "database/sql" "time" "github.com/google/uuid" ) type BaseEntity struct { - ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:uuidv7()"` CreatedAt time.Time UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt sql.NullTime } func (be BaseEntity) IsZero() bool { return be.ID == uuid.Nil } - -func (be BaseEntity) IsDeleted() bool { - return be.DeletedAt.Valid -} From d8d51f32f3524572694fce13c86f12696a502527 Mon Sep 17 00:00:00 2001 From: itsLeonB Date: Sun, 4 Jan 2026 14:58:24 +0700 Subject: [PATCH 2/6] chore: bump ezutil and ungerr --- go.mod | 11 +++++++---- go.sum | 26 ++++++++++++++++++-------- gorm_repository.go | 26 +++++++++++++------------- gorm_scopes.go | 4 ++-- internal/transactor.go | 12 ++++++------ 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index 75fb6fa..85bbde9 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.25.0 require ( github.com/google/uuid v1.6.0 - github.com/itsLeonB/ezutil/v2 v2.0.0 - github.com/rotisserie/eris v0.5.4 - github.com/stretchr/testify v1.11.0 + github.com/itsLeonB/ezutil/v2 v2.2.1-pre1 + github.com/itsLeonB/ungerr v0.2.0-pre3 + github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.1 @@ -20,7 +20,10 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect - golang.org/x/text v0.24.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + golang.org/x/text v0.27.0 // indirect + google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2e98e00..5fd3c6f 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,14 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/itsLeonB/ezutil/v2 v2.0.0 h1:4o6nVMzCIr56ggXifDG7Mr+ucDDUg3bVeuiNjsFVcRI= -github.com/itsLeonB/ezutil/v2 v2.0.0/go.mod h1:fiUusldH3h+Y3vYimdloT+CBVO2AKT0xEtXvSHgvTts= +github.com/itsLeonB/ezutil/v2 v2.2.1-pre1 h1:MEHSCy4qToEuhXSOyKdVfl9+iJWXbPALO+o4YqMM0fs= +github.com/itsLeonB/ezutil/v2 v2.2.1-pre1/go.mod h1:485McANphv4ws3sPTli8HC9UQitkR6jBINzzP8sMLSU= +github.com/itsLeonB/ungerr v0.2.0-pre3 h1:3ZvWc7blEHMfUPQPcMTmY2wgw8PiwAJYgxnkB1j0ids= +github.com/itsLeonB/ungerr v0.2.0-pre3/go.mod h1:CGA9XktVW4iE5afcM/qZxHG/iVUUEPg3hklBvZnfrGg= 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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -24,14 +28,20 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= -github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= -github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= -github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 h1:Nm5SEGIguOIBDXs5rhfz2aKwEVWlgwC58UcmEnLDc8Y= +google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1/go.mod h1:Jz9LrroM7Mcm+a0QrLh4UpZ1B/WhjIbqwEcUf4y08nQ= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/gorm_repository.go b/gorm_repository.go index b005f66..d8a6bec 100644 --- a/gorm_repository.go +++ b/gorm_repository.go @@ -4,7 +4,7 @@ import ( "context" "reflect" - "github.com/rotisserie/eris" + "github.com/itsLeonB/ungerr" "gorm.io/gorm" ) @@ -68,7 +68,7 @@ func (gr *gormRepository[T]) Insert(ctx context.Context, model T) (T, error) { } if err = db.Create(&model).Error; err != nil { - return zero, eris.Wrap(err, "error inserting data") + return zero, ungerr.Wrap(err, "error inserting data") } return model, nil @@ -93,7 +93,7 @@ func (gr *gormRepository[T]) FindAll(ctx context.Context, spec Specification[T]) Error if err != nil { - return nil, eris.Wrap(err, "error querying data") + return nil, ungerr.Wrap(err, "error querying data") } return models, nil @@ -121,7 +121,7 @@ func (gr *gormRepository[T]) FindFirst(ctx context.Context, spec Specification[T if err == gorm.ErrRecordNotFound { return model, nil } - return model, eris.Wrap(err, "error querying data") + return model, ungerr.Wrap(err, "error querying data") } return model, nil @@ -140,7 +140,7 @@ func (gr *gormRepository[T]) Update(ctx context.Context, model T) (T, error) { } if err = db.Save(&model).Error; err != nil { - return zero, eris.Wrap(err, "error updating data") + return zero, ungerr.Wrap(err, "error updating data") } return model, nil @@ -157,7 +157,7 @@ func (gr *gormRepository[T]) Delete(ctx context.Context, model T) error { } if err = db.Unscoped().Delete(&model).Error; err != nil { - return eris.Wrap(err, "error deleting data") + return ungerr.Wrap(err, "error deleting data") } return nil @@ -165,7 +165,7 @@ func (gr *gormRepository[T]) Delete(ctx context.Context, model T) error { func (gr *gormRepository[T]) InsertMany(ctx context.Context, models []T) ([]T, error) { if len(models) < 1 { - return nil, eris.Errorf("inserted models cannot be empty") + return nil, ungerr.Unknown("inserted models cannot be empty") } db, err := gr.GetGormInstance(ctx) @@ -174,7 +174,7 @@ func (gr *gormRepository[T]) InsertMany(ctx context.Context, models []T) ([]T, e } if err = db.Create(&models).Error; err != nil { - return nil, eris.Wrap(err, "error batch inserting data") + return nil, ungerr.Wrap(err, "error batch inserting data") } return models, nil @@ -182,7 +182,7 @@ func (gr *gormRepository[T]) InsertMany(ctx context.Context, models []T) ([]T, e func (gr *gormRepository[T]) DeleteMany(ctx context.Context, models []T) error { if len(models) < 1 { - return eris.Errorf("deleted models cannot be empty") + return ungerr.Unknown("deleted models cannot be empty") } db, err := gr.GetGormInstance(ctx) @@ -191,7 +191,7 @@ func (gr *gormRepository[T]) DeleteMany(ctx context.Context, models []T) error { } if err = db.Unscoped().Delete(&models).Error; err != nil { - return eris.Wrap(err, "error batch deleting data") + return ungerr.Wrap(err, "error batch deleting data") } return nil @@ -199,7 +199,7 @@ func (gr *gormRepository[T]) DeleteMany(ctx context.Context, models []T) error { func (gr *gormRepository[T]) SaveMany(ctx context.Context, models []T) ([]T, error) { if len(models) < 1 { - return nil, eris.Errorf("saved models cannot be empty") + return nil, ungerr.Unknown("saved models cannot be empty") } db, err := gr.GetGormInstance(ctx) @@ -208,7 +208,7 @@ func (gr *gormRepository[T]) SaveMany(ctx context.Context, models []T) ([]T, err } if err = db.Save(&models).Error; err != nil { - return nil, eris.Wrap(err, "error saving many data") + return nil, ungerr.Wrap(err, "error saving many data") } return models, nil @@ -216,7 +216,7 @@ func (gr *gormRepository[T]) SaveMany(ctx context.Context, models []T) ([]T, err func (gr *gormRepository[T]) checkZeroValue(model T) error { if reflect.DeepEqual(model, *new(T)) { - return eris.New("model cannot be zero value") + return ungerr.Unknown("model cannot be zero value") } return nil diff --git a/gorm_scopes.go b/gorm_scopes.go index e4011c2..6d28aa4 100644 --- a/gorm_scopes.go +++ b/gorm_scopes.go @@ -6,7 +6,7 @@ import ( "github.com/itsLeonB/ezutil/v2" "github.com/itsLeonB/go-crud/internal" - "github.com/rotisserie/eris" + "github.com/itsLeonB/ungerr" "gorm.io/gorm" "gorm.io/gorm/clause" ) @@ -34,7 +34,7 @@ func OrderBy(field string, ascending bool) func(db *gorm.DB) *gorm.DB { // Basic validation to prevent SQL injection // Only allow alphanumeric characters, underscores, and dots for table.column if !internal.IsValidFieldName(field) { - _ = db.AddError(eris.Errorf("invalid field name: %s", field)) + _ = db.AddError(ungerr.Unknownf("invalid field name: %s", field)) return db } diff --git a/internal/transactor.go b/internal/transactor.go index b0d3673..53bc549 100644 --- a/internal/transactor.go +++ b/internal/transactor.go @@ -5,7 +5,7 @@ import ( "log" "github.com/itsLeonB/go-crud/lib" - "github.com/rotisserie/eris" + "github.com/itsLeonB/ungerr" "gorm.io/gorm" ) @@ -16,7 +16,7 @@ type GormTransactor struct { func (t *GormTransactor) Begin(ctx context.Context) (context.Context, error) { tx := t.DB.WithContext(ctx).Begin() if err := tx.Error; err != nil { - return nil, eris.Wrap(err, lib.MsgTransactionError) + return nil, ungerr.Wrap(err, lib.MsgTransactionError) } return context.WithValue(ctx, lib.ContextKeyGormTx, tx), nil @@ -30,7 +30,7 @@ func (t *GormTransactor) Commit(ctx context.Context) error { if tx != nil { err = tx.WithContext(ctx).Commit().Error if err != nil { - return eris.Wrap(err, lib.MsgTransactionError) + return ungerr.Wrap(err, lib.MsgTransactionError) } } @@ -63,7 +63,7 @@ func (t *GormTransactor) WithinTransaction(ctx context.Context, serviceFn func(c // Check if we're already within a transaction existingTx, err := GetTxFromContext(ctx) if err != nil { - return eris.Wrap(err, "error checking existing transaction") + return ungerr.Wrap(err, "error checking existing transaction") } // If we're already in a transaction, just execute the service function @@ -74,7 +74,7 @@ func (t *GormTransactor) WithinTransaction(ctx context.Context, serviceFn func(c // Start a new transaction ctx, err = t.Begin(ctx) if err != nil { - return eris.Wrap(err, "error starting transaction") + return ungerr.Wrap(err, "error starting transaction") } defer t.Rollback(ctx) @@ -90,7 +90,7 @@ func GetTxFromContext(ctx context.Context) (*gorm.DB, error) { if trx != nil { tx, ok := trx.(*gorm.DB) if !ok { - return nil, eris.New("error getting tx from ctx") + return nil, ungerr.Unknown("error getting tx from ctx") } return tx, nil From 30697813e6d438e220f7f24748941fece723f18f Mon Sep 17 00:00:00 2001 From: itsLeonB Date: Mon, 5 Jan 2026 14:23:26 +0700 Subject: [PATCH 3/6] fix: remove deleted at scope --- gorm_repository.go | 3 --- gorm_scopes.go | 19 ------------------ internal/gorm_scopes.go | 31 ----------------------------- test/scopes_test.go | 43 ----------------------------------------- 4 files changed, 96 deletions(-) delete mode 100644 internal/gorm_scopes.go diff --git a/gorm_repository.go b/gorm_repository.go index d8a6bec..5df8e90 100644 --- a/gorm_repository.go +++ b/gorm_repository.go @@ -38,7 +38,6 @@ type Specification[T any] struct { Model T // Model with fields set for WHERE conditions PreloadRelations []string // Relations to eager load ForUpdate bool // Whether to use SELECT ... FOR UPDATE - DeletedFilter DeletedFilter } // NewRepository creates a new CRUD repository implementation using GORM. @@ -87,7 +86,6 @@ func (gr *gormRepository[T]) FindAll(ctx context.Context, spec Specification[T]) DefaultOrder(), PreloadRelations(spec.PreloadRelations), ForUpdate(spec.ForUpdate), - spec.DeletedFilter.WhereDeleted(), ). Find(&models). Error @@ -112,7 +110,6 @@ func (gr *gormRepository[T]) FindFirst(ctx context.Context, spec Specification[T DefaultOrder(), PreloadRelations(spec.PreloadRelations), ForUpdate(spec.ForUpdate), - spec.DeletedFilter.WhereDeleted(), ). First(&model). Error diff --git a/gorm_scopes.go b/gorm_scopes.go index 6d28aa4..dd55742 100644 --- a/gorm_scopes.go +++ b/gorm_scopes.go @@ -106,22 +106,3 @@ func ForUpdate(enable bool) func(*gorm.DB) *gorm.DB { return db } } - -type DeletedFilter struct { - filterType internal.DeletedFilterType -} - -func (df *DeletedFilter) WhereDeleted() func(*gorm.DB) *gorm.DB { - return func(d *gorm.DB) *gorm.DB { - if df.filterType == nil { - return d - } - return df.filterType.WhereDeleted()(d) - } -} - -var ( - ExcludeDeleted DeletedFilter = DeletedFilter{internal.ExcludeDeleted{}} - IncludeDeleted DeletedFilter = DeletedFilter{internal.IncludeDeleted{}} - OnlyDeleted DeletedFilter = DeletedFilter{internal.OnlyDeleted{}} -) diff --git a/internal/gorm_scopes.go b/internal/gorm_scopes.go deleted file mode 100644 index f336b68..0000000 --- a/internal/gorm_scopes.go +++ /dev/null @@ -1,31 +0,0 @@ -package internal - -import "gorm.io/gorm" - -type DeletedFilterType interface { - WhereDeleted() func(*gorm.DB) *gorm.DB -} - -type ExcludeDeleted struct{} - -func (ed ExcludeDeleted) WhereDeleted() func(*gorm.DB) *gorm.DB { - return func(d *gorm.DB) *gorm.DB { - return d.Where("deleted_at IS NULL") - } -} - -type IncludeDeleted struct{} - -func (id IncludeDeleted) WhereDeleted() func(*gorm.DB) *gorm.DB { - return func(d *gorm.DB) *gorm.DB { - return d - } -} - -type OnlyDeleted struct{} - -func (od OnlyDeleted) WhereDeleted() func(*gorm.DB) *gorm.DB { - return func(d *gorm.DB) *gorm.DB { - return d.Where("deleted_at IS NOT NULL") - } -} diff --git a/test/scopes_test.go b/test/scopes_test.go index 06d1cfa..0900795 100644 --- a/test/scopes_test.go +++ b/test/scopes_test.go @@ -280,46 +280,3 @@ func TestForUpdate(t *testing.T) { }) } } -func TestDeletedFilter(t *testing.T) { - db := setupScopesTestDB(t) - - // Insert test data - models := []SoftDeleteModel{ - {Name: "Active1"}, - {Name: "Active2"}, - {Name: "ToDelete1"}, - {Name: "ToDelete2"}, - } - err := db.Create(&models).Error - assert.NoError(t, err, "Failed to create test data") - - // Soft delete some records - err = db.Delete(&models[2]).Error - assert.NoError(t, err, "Failed to soft delete record") - err = db.Delete(&models[3]).Error - assert.NoError(t, err, "Failed to soft delete record") - - tests := []struct { - name string - filter crud.DeletedFilter - useUnscoped bool - wantCount int - }{ - {"exclude deleted", crud.ExcludeDeleted, false, 2}, - {"include deleted", crud.IncludeDeleted, true, 4}, - {"only deleted", crud.OnlyDeleted, true, 2}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var results []SoftDeleteModel - query := db.Scopes(tt.filter.WhereDeleted()) - if tt.useUnscoped { - query = query.Unscoped() - } - err := query.Find(&results).Error - assert.NoError(t, err, "DeletedFilter should not return error") - assert.Len(t, results, tt.wantCount, "DeletedFilter should return expected number of records") - }) - } -} From 92b1303aeba0a79f294f417e77a72892366702d7 Mon Sep 17 00:00:00 2001 From: itsLeonB Date: Fri, 9 Jan 2026 15:36:54 +0700 Subject: [PATCH 4/6] test: adjust command and add coverage --- .github/workflows/ci.yml | 4 - Makefile | 91 ++-- go.mod | 10 +- go.sum | 38 +- gorm_repository_test.go | 471 ++++++++++++++++++ test/scopes_test.go => gorm_scopes_test.go | 12 +- ...nsactor_test.go => gorm_transactor_test.go | 85 ++-- {test => internal}/internal_test.go | 2 +- scripts/git-pre-push.sh | 10 +- test/constants_test.go | 91 ---- test/repository_test.go | 250 ---------- 11 files changed, 607 insertions(+), 457 deletions(-) create mode 100644 gorm_repository_test.go rename test/scopes_test.go => gorm_scopes_test.go (97%) rename test/transactor_test.go => gorm_transactor_test.go (74%) rename {test => internal}/internal_test.go (99%) delete mode 100644 test/constants_test.go delete mode 100644 test/repository_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8d0e9d..90039ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,6 @@ jobs: - name: Verify dependencies run: | go mod verify - cd test && go mod verify - name: Run tests run: make test-coverage @@ -87,9 +86,6 @@ jobs: - name: Build run: go build -v ./... - - name: Build test package - run: cd test && go build -v ./... - security: name: Security Scan runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 06b6411..2b99668 100644 --- a/Makefile +++ b/Makefile @@ -1,76 +1,54 @@ -TEST_DIR := ./test -COVER_PKG := ./... - -.PHONY: help \ -lint \ -test \ -test-verbose \ -test-coverage \ -test-coverage-html \ -test-clean \ -mock \ -install-pre-push-hook \ -uninstall-pre-push-hook +.PHONY: help lint build test test-verbose test-coverage test-coverage-html test-clean install-pre-push-hook uninstall-pre-push-hook mock help: - @echo "Makefile commands:" - @echo " make lint - Run golangci-lint on the codebase" - @echo " make test - Run all tests" - @echo " make test-verbose - Run all tests with verbose output" - @echo " make test-coverage - Run all tests with coverage report" - @echo " make test-coverage-html - Run all tests and generate HTML coverage report" - @echo " make test-clean - Clean test cache and run tests" - @echo " make mock - Generate mock implementations" - @echo " make install-pre-push-hook - Install the pre-push git hook" - @echo " make uninstall-pre-push-hook - Uninstall the pre-push git hook" + @echo "Available commands:" + @echo " help - Show this help message" + @echo " lint - Run golangci-lint on the codebase" + @echo " build - Build the project" + @echo " test - Run all tests" + @echo " test-verbose - Run all tests with verbose output" + @echo " test-coverage - Run tests with coverage report for each package" + @echo " test-coverage-html - Run tests and generate HTML coverage reports for each package" + @echo " test-clean - Clean test cache and run tests" + @echo " install-pre-push-hook - Install the pre-push git hook" + @echo " uninstall-pre-push-hook - Uninstall the pre-push git hook" + @echo " mock - Generate mocks" lint: golangci-lint run ./... +build: + go build -v ./... + test: @echo "Running all tests..." - @if [ -d $(TEST_DIR) ]; then \ - go test $(TEST_DIR)/...; \ - else \ - echo "No tests found in $(TEST_DIR), skipping."; \ - fi + go test ./... test-verbose: @echo "Running all tests with verbose output..." - @if [ -d $(TEST_DIR) ]; then \ - go test -v $(TEST_DIR)/...; \ - else \ - echo "No tests found in $(TEST_DIR), skipping."; \ - fi + go test -v ./... test-coverage: - @echo "Running all tests with coverage report..." - @if [ -d $(TEST_DIR) ]; then \ - go test -v -cover -coverprofile=coverage.out -coverpkg=$(COVER_PKG) $(TEST_DIR)/...; \ - else \ - echo "No tests found in $(TEST_DIR), skipping."; \ - fi + @echo "Running tests with coverage report for each package..." + @for pkg in $$(go list ./...); do \ + pkgname=$$(echo $$pkg | tr '/' '-'); \ + echo "Coverage for $$pkg:"; \ + go test -v -coverpkg=$$pkg -coverprofile=coverage-$$pkgname.out $$pkg; \ + done test-coverage-html: - @echo "Running all tests and generating HTML coverage report..." - @if [ -d $(TEST_DIR) ]; then \ - go test -v -cover -coverprofile=coverage.out -coverpkg=$(COVER_PKG) $(TEST_DIR)/... && \ - go tool cover -html=coverage.out -o coverage.html && \ - echo "Coverage report generated: coverage.html"; \ - else \ - echo "No tests found in $(TEST_DIR), skipping."; \ - fi + @echo "Running tests and generating HTML coverage reports for each package..." + @for pkg in $$(go list ./...); do \ + pkgname=$$(echo $$pkg | tr '/' '-'); \ + echo "Coverage for $$pkg:"; \ + go test -v -coverpkg=$$pkg -coverprofile=coverage-$$pkgname.out $$pkg; \ + go tool cover -html=coverage-$$pkgname.out -o coverage-$$pkgname.html; \ + done + @echo "Coverage reports generated: coverage-*.html" test-clean: @echo "Cleaning test cache and running tests..." - @if [ -d $(TEST_DIR) ]; then \ - go clean -testcache && go test -v $(TEST_DIR)/...; \ - else \ - echo "No tests found in $(TEST_DIR), skipping."; \ - fi - -mock: - @scripts/gen-mocks.sh + go clean -testcache && go test -v ./... install-pre-push-hook: @echo "Installing pre-push git hook..." @@ -83,3 +61,6 @@ uninstall-pre-push-hook: @echo "Uninstalling pre-push git hook..." @rm -f .git/hooks/pre-push @echo "Pre-push hook uninstalled successfully!" + +mock: + @scripts/gen-mocks.sh diff --git a/go.mod b/go.mod index 85bbde9..98a632a 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.25.0 require ( github.com/google/uuid v1.6.0 - github.com/itsLeonB/ezutil/v2 v2.2.1-pre1 - github.com/itsLeonB/ungerr v0.2.0-pre3 + github.com/itsLeonB/ezutil/v2 v2.2.1 + github.com/itsLeonB/ungerr v0.2.0 github.com/stretchr/testify v1.11.1 go.uber.org/mock v0.6.0 gorm.io/driver/sqlite v1.6.0 @@ -18,9 +18,13 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/kr/pretty v0.3.0 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/mfridman/interpolate v0.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/pressly/goose/v3 v3.26.0 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sync v0.16.0 // indirect golang.org/x/text v0.27.0 // indirect google.golang.org/genproto v0.0.0-20250826171959-ef028d996bc1 // indirect google.golang.org/protobuf v1.36.8 // indirect diff --git a/go.sum b/go.sum index 5fd3c6f..c66afe0 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,16 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/itsLeonB/ezutil/v2 v2.2.1-pre1 h1:MEHSCy4qToEuhXSOyKdVfl9+iJWXbPALO+o4YqMM0fs= -github.com/itsLeonB/ezutil/v2 v2.2.1-pre1/go.mod h1:485McANphv4ws3sPTli8HC9UQitkR6jBINzzP8sMLSU= -github.com/itsLeonB/ungerr v0.2.0-pre3 h1:3ZvWc7blEHMfUPQPcMTmY2wgw8PiwAJYgxnkB1j0ids= -github.com/itsLeonB/ungerr v0.2.0-pre3/go.mod h1:CGA9XktVW4iE5afcM/qZxHG/iVUUEPg3hklBvZnfrGg= +github.com/itsLeonB/ezutil/v2 v2.2.1 h1:hXjLOTrZoD1BYkd/jYErx+cSwrwrW7ay6TX0UozMD98= +github.com/itsLeonB/ezutil/v2 v2.2.1/go.mod h1:b1L2eUJtM5z5Kf+zXVPoVtSpjCBLt4CUDVhGQWz0bsI= +github.com/itsLeonB/ungerr v0.2.0 h1:+IMzMcgWuywsnASGzzWamqHdgNxZPYMOg4SoZ61XA0I= +github.com/itsLeonB/ungerr v0.2.0/go.mod h1:CGA9XktVW4iE5afcM/qZxHG/iVUUEPg3hklBvZnfrGg= 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.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= @@ -21,19 +23,39 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= +github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -53,3 +75,11 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4= gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= diff --git a/gorm_repository_test.go b/gorm_repository_test.go new file mode 100644 index 0000000..d787ffc --- /dev/null +++ b/gorm_repository_test.go @@ -0,0 +1,471 @@ +package crud_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + crud "github.com/itsLeonB/go-crud" + "github.com/stretchr/testify/assert" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// TestUser represents a user entity for testing +type TestUser struct { + ID uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + Email string `gorm:"unique"` + Age int + CreatedAt time.Time + UpdatedAt time.Time +} + +// TestProfile represents a profile entity for testing +type TestProfile struct { + ID uint `gorm:"primaryKey"` + UserID uint + Bio string + User TestUser `gorm:"foreignKey:UserID"` +} + +// TestEntityWithBase for testing BaseEntity +type TestEntityWithBase struct { + crud.BaseEntity + Name string +} + +func TestBaseEntity_IsZero(t *testing.T) { + // Test zero value + var entity TestEntityWithBase + assert.True(t, entity.IsZero()) + + // Test non-zero value + entity.ID = uuid.New() + assert.False(t, entity.IsZero()) + + // Test with CreatedAt set but ID still zero + entity = TestEntityWithBase{} + entity.CreatedAt = time.Now() + assert.True(t, entity.IsZero()) // IsZero only checks ID +} + +func setupTestDB() *gorm.DB { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + if err != nil { + panic("failed to connect database") + } + + // Migrate the schema + if err = db.AutoMigrate(&TestUser{}, &TestProfile{}); err != nil { + panic(err) + } + + return db +} + +func TestCRUDRepository_Insert(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + user := TestUser{ + Name: "John Doe", + Email: "john@example.com", + Age: 30, + } + + result, err := repo.Insert(ctx, user) + + assert.NoError(t, err) + assert.NotZero(t, result.ID) + assert.Equal(t, "John Doe", result.Name) + assert.Equal(t, "john@example.com", result.Email) + assert.Equal(t, 30, result.Age) + assert.NotZero(t, result.CreatedAt) + assert.NotZero(t, result.UpdatedAt) +} + +func TestCRUDRepository_FindAll(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert test data + users := []TestUser{ + {Name: "Alice", Email: "alice@example.com", Age: 25}, + {Name: "Bob", Email: "bob@example.com", Age: 30}, + {Name: "Charlie", Email: "charlie@example.com", Age: 35}, + } + + for _, user := range users { + _, err := repo.Insert(ctx, user) + assert.NoError(t, err) + } + + // Test finding all users + spec := crud.Specification[TestUser]{} + result, err := repo.FindAll(ctx, spec) + + assert.NoError(t, err) + assert.Len(t, result, 3) +} + +func TestCRUDRepository_FindAll_WithSpecification(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert test data + users := []TestUser{ + {Name: "Alice", Email: "alice@example.com", Age: 25}, + {Name: "Bob", Email: "bob@example.com", Age: 30}, + {Name: "Charlie", Email: "charlie@example.com", Age: 25}, + } + + for _, user := range users { + _, err := repo.Insert(ctx, user) + assert.NoError(t, err) + } + + // Test finding users with age 25 + spec := crud.Specification[TestUser]{ + Model: TestUser{Age: 25}, + } + result, err := repo.FindAll(ctx, spec) + + assert.NoError(t, err) + assert.Len(t, result, 2) + for _, user := range result { + assert.Equal(t, 25, user.Age) + } +} + +func TestCRUDRepository_FindFirst(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert test data + user := TestUser{ + Name: "John Doe", + Email: "john@example.com", + Age: 30, + } + insertedUser, err := repo.Insert(ctx, user) + assert.NoError(t, err) + + // Test finding first user + spec := crud.Specification[TestUser]{ + Model: TestUser{ID: insertedUser.ID}, + } + result, err := repo.FindFirst(ctx, spec) + + assert.NoError(t, err) + assert.Equal(t, insertedUser.ID, result.ID) + assert.Equal(t, "John Doe", result.Name) + assert.Equal(t, "john@example.com", result.Email) + assert.Equal(t, 30, result.Age) +} + +func TestCRUDRepository_FindFirst_NotFound(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Test finding non-existent user + spec := crud.Specification[TestUser]{ + Model: TestUser{ID: 999}, + } + result, err := repo.FindFirst(ctx, spec) + + // Check if error is returned or if result is zero value + if err != nil { + assert.Error(t, err) + assert.Contains(t, err.Error(), "record not found") + } else { + // If no error, result should be zero value + assert.Zero(t, result.ID) + } +} + +func TestCRUDRepository_Update(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert test data + user := TestUser{ + Name: "John Doe", + Email: "john@example.com", + Age: 30, + } + insertedUser, err := repo.Insert(ctx, user) + assert.NoError(t, err) + + // Update user + insertedUser.Name = "John Smith" + insertedUser.Age = 31 + result, err := repo.Update(ctx, insertedUser) + + assert.NoError(t, err) + assert.Equal(t, insertedUser.ID, result.ID) + assert.Equal(t, "John Smith", result.Name) + assert.Equal(t, "john@example.com", result.Email) + assert.Equal(t, 31, result.Age) + assert.True(t, result.UpdatedAt.After(result.CreatedAt)) +} + +func TestCRUDRepository_Delete(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert test data + user := TestUser{ + Name: "John Doe", + Email: "john@example.com", + Age: 30, + } + insertedUser, err := repo.Insert(ctx, user) + assert.NoError(t, err) + + // Delete user + err = repo.Delete(ctx, insertedUser) + assert.NoError(t, err) + + // Verify user is deleted + spec := crud.Specification[TestUser]{ + Model: TestUser{ID: insertedUser.ID}, + } + result, err := repo.FindFirst(ctx, spec) + + // Check if error is returned or if result is zero value + if err != nil { + assert.Error(t, err) + assert.Contains(t, err.Error(), "record not found") + } else { + // If no error, result should be zero value (deleted) + assert.Zero(t, result.ID) + } +} + +func TestCRUDRepository_InsertMany(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + users := []TestUser{ + {Name: "Alice", Email: "alice@example.com", Age: 25}, + {Name: "Bob", Email: "bob@example.com", Age: 30}, + {Name: "Charlie", Email: "charlie@example.com", Age: 35}, + } + + result, err := repo.InsertMany(ctx, users) + + assert.NoError(t, err) + assert.Len(t, result, 3) + for i, user := range result { + assert.NotZero(t, user.ID) + assert.Equal(t, users[i].Name, user.Name) + assert.Equal(t, users[i].Email, user.Email) + assert.Equal(t, users[i].Age, user.Age) + assert.NotZero(t, user.CreatedAt) + assert.NotZero(t, user.UpdatedAt) + } +} + +func TestCRUDRepository_InsertMany_Empty(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + var users []TestUser + + result, err := repo.InsertMany(ctx, users) + + // The implementation returns an error for empty slice, which is expected behavior + assert.Error(t, err) + assert.Contains(t, err.Error(), "inserted models cannot be empty") + assert.Len(t, result, 0) +} + +func TestCRUDRepository_GetGormInstance(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + gormDB, err := repo.GetGormInstance(ctx) + + assert.NoError(t, err) + assert.NotNil(t, gormDB) + // Just check that we get a valid GORM instance, don't compare directly + assert.IsType(t, &gorm.DB{}, gormDB) +} + +func TestCRUDRepository_WithPreloadRelations(t *testing.T) { + db := setupTestDB() + userRepo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert test data + user := TestUser{ + Name: "John Doe", + Email: "john@example.com", + Age: 30, + } + insertedUser, err := userRepo.Insert(ctx, user) + assert.NoError(t, err) + + // Test finding user with empty preload relations (should work) + spec := crud.Specification[TestUser]{ + Model: TestUser{ID: insertedUser.ID}, + PreloadRelations: []string{}, // Empty relations should work + } + result, err := userRepo.FindFirst(ctx, spec) + + assert.NoError(t, err) + assert.Equal(t, insertedUser.ID, result.ID) +} + +func TestCRUDRepository_WithForUpdate(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert test data + user := TestUser{ + Name: "John Doe", + Email: "john@example.com", + Age: 30, + } + insertedUser, err := repo.Insert(ctx, user) + assert.NoError(t, err) + + // Test finding user with FOR UPDATE + spec := crud.Specification[TestUser]{ + Model: TestUser{ID: insertedUser.ID}, + ForUpdate: true, + } + result, err := repo.FindFirst(ctx, spec) + + assert.NoError(t, err) + assert.Equal(t, insertedUser.ID, result.ID) +} + +func TestCRUDRepository_DeleteMany(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert test data + users := []TestUser{ + {Name: "Alice", Email: "alice@example.com", Age: 25}, + {Name: "Bob", Email: "bob@example.com", Age: 30}, + {Name: "Charlie", Email: "charlie@example.com", Age: 35}, + } + insertedUsers, err := repo.InsertMany(ctx, users) + assert.NoError(t, err) + + // Delete multiple users + err = repo.DeleteMany(ctx, insertedUsers) + assert.NoError(t, err) + + // Verify users are deleted + spec := crud.Specification[TestUser]{} + results, err := repo.FindAll(ctx, spec) + assert.NoError(t, err) + assert.Len(t, results, 0) +} + +func TestCRUDRepository_DeleteMany_Empty(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + var users []TestUser + err := repo.DeleteMany(ctx, users) + assert.Error(t, err) + assert.Contains(t, err.Error(), "deleted models cannot be empty") +} + +func TestCRUDRepository_SaveMany(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert initial data + users := []TestUser{ + {Name: "Alice", Email: "alice@example.com", Age: 25}, + {Name: "Bob", Email: "bob@example.com", Age: 30}, + } + insertedUsers, err := repo.InsertMany(ctx, users) + assert.NoError(t, err) + + // Modify and save + insertedUsers[0].Age = 26 + insertedUsers[1].Age = 31 + savedUsers, err := repo.SaveMany(ctx, insertedUsers) + assert.NoError(t, err) + assert.Len(t, savedUsers, 2) + assert.Equal(t, 26, savedUsers[0].Age) + assert.Equal(t, 31, savedUsers[1].Age) +} + +func TestCRUDRepository_SaveMany_Empty(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + var users []TestUser + result, err := repo.SaveMany(ctx, users) + assert.Error(t, err) + assert.Contains(t, err.Error(), "saved models cannot be empty") + assert.Len(t, result, 0) +} + +func TestCRUDRepository_Insert_Error(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert user with duplicate email should fail + user1 := TestUser{Name: "Alice", Email: "test@example.com", Age: 25} + _, err := repo.Insert(ctx, user1) + assert.NoError(t, err) + + user2 := TestUser{Name: "Bob", Email: "test@example.com", Age: 30} + _, err = repo.Insert(ctx, user2) + assert.Error(t, err) +} + +func TestCRUDRepository_Update_Error(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Insert a user first to get a valid ID + user := TestUser{Name: "Test", Email: "test@example.com", Age: 25} + inserted, err := repo.Insert(ctx, user) + assert.NoError(t, err) + + // Update should work normally + inserted.Age = 26 + result, err := repo.Update(ctx, inserted) + assert.NoError(t, err) + assert.Equal(t, inserted.ID, result.ID) + assert.Equal(t, 26, result.Age) +} + +func TestCRUDRepository_Delete_Error(t *testing.T) { + db := setupTestDB() + repo := crud.NewRepository[TestUser](db) + ctx := context.Background() + + // Try to delete with zero ID should error due to WHERE conditions required + user := TestUser{ID: 0, Name: "Test", Email: "test@example.com", Age: 25} + err := repo.Delete(ctx, user) + assert.Error(t, err) + assert.Contains(t, err.Error(), "WHERE conditions required") +} diff --git a/test/scopes_test.go b/gorm_scopes_test.go similarity index 97% rename from test/scopes_test.go rename to gorm_scopes_test.go index 0900795..382ada9 100644 --- a/test/scopes_test.go +++ b/gorm_scopes_test.go @@ -1,4 +1,4 @@ -package gocrud_test +package crud_test import ( "testing" @@ -11,6 +11,16 @@ import ( "gorm.io/gorm/logger" ) +// TestModel for testing scopes +type TestModel struct { + ID uint `gorm:"primaryKey"` + Name string `gorm:"not null"` + Email string `gorm:"unique"` + Age int + CreatedAt time.Time + UpdatedAt time.Time +} + // SoftDeleteModel for testing soft delete functionality type SoftDeleteModel struct { ID uint `gorm:"primaryKey"` diff --git a/test/transactor_test.go b/gorm_transactor_test.go similarity index 74% rename from test/transactor_test.go rename to gorm_transactor_test.go index 8661002..17e2d81 100644 --- a/test/transactor_test.go +++ b/gorm_transactor_test.go @@ -1,4 +1,4 @@ -package gocrud_test +package crud_test import ( "context" @@ -18,7 +18,7 @@ func setupTransactorTestDB(t *testing.T) *gorm.DB { }) assert.NoError(t, err, "Failed to connect to test database") - err = db.AutoMigrate(&TestModel{}) + err = db.AutoMigrate(&TestUser{}) assert.NoError(t, err, "Failed to migrate test models") return db @@ -39,11 +39,6 @@ func TestTransactor_Begin(t *testing.T) { txCtx, err := transactor.Begin(ctx) assert.NoError(t, err, "Begin should not return error") assert.NotNil(t, txCtx, "Begin should not return nil context") - - // Verify transaction is in context - tx, err := crud.GetTxFromContext(txCtx) - assert.NoError(t, err, "GetTxFromContext should not return error") - assert.NotNil(t, tx, "Begin should store transaction in context") } func TestTransactor_Commit(t *testing.T) { @@ -91,13 +86,13 @@ func TestTransactor_Rollback(t *testing.T) { func TestTransactor_WithinTransaction_Success(t *testing.T) { db := setupTransactorTestDB(t) transactor := crud.NewTransactor(db) - repo := crud.NewRepository[TestModel](db) + repo := crud.NewRepository[TestUser](db) ctx := context.Background() var insertedID uint err := transactor.WithinTransaction(ctx, func(txCtx context.Context) error { - model := TestModel{ + model := TestUser{ Name: "Alice", Email: "alice@example.com", Age: 25, @@ -115,8 +110,8 @@ func TestTransactor_WithinTransaction_Success(t *testing.T) { assert.NoError(t, err, "WithinTransaction should not return error") // Verify the record was committed - spec := crud.Specification[TestModel]{ - Model: TestModel{ID: insertedID}, + spec := crud.Specification[TestUser]{ + Model: TestUser{ID: insertedID}, } result, err := repo.FindFirst(ctx, spec) assert.NoError(t, err, "Error verifying committed record") @@ -127,50 +122,46 @@ func TestTransactor_WithinTransaction_Success(t *testing.T) { func TestTransactor_WithinTransaction_Rollback(t *testing.T) { db := setupTransactorTestDB(t) transactor := crud.NewTransactor(db) - repo := crud.NewRepository[TestModel](db) + repo := crud.NewRepository[TestUser](db) ctx := context.Background() - var insertedID uint expectedError := errors.New("service error") err := transactor.WithinTransaction(ctx, func(txCtx context.Context) error { - model := TestModel{ + model := TestUser{ Name: "Bob", Email: "bob@example.com", Age: 30, } - result, err := repo.Insert(txCtx, model) + _, err := repo.Insert(txCtx, model) if err != nil { return err } - insertedID = result.ID return expectedError }) assert.Error(t, err, "WithinTransaction should return error") assert.Equal(t, expectedError, err, "WithinTransaction should return the expected error") - // Verify the record was rolled back - spec := crud.Specification[TestModel]{ - Model: TestModel{ID: insertedID}, - } - result, err := repo.FindFirst(ctx, spec) - assert.NoError(t, err, "Error checking rolled back record") - assert.Zero(t, result.ID, "WithinTransaction record should be rolled back") + // Verify no records exist (rollback worked) + spec := crud.Specification[TestUser]{} + results, err := repo.FindAll(ctx, spec) + assert.NoError(t, err) + assert.Len(t, results, 0, "WithinTransaction should rollback all records") } func TestTransactor_WithinTransaction_Nested(t *testing.T) { db := setupTransactorTestDB(t) transactor := crud.NewTransactor(db) - repo := crud.NewRepository[TestModel](db) + repo := crud.NewRepository[TestUser](db) ctx := context.Background() var outerID, innerID uint err := transactor.WithinTransaction(ctx, func(outerTxCtx context.Context) error { - outerModel := TestModel{ + outerModel := TestUser{ Name: "Outer", Email: "outer@example.com", Age: 25, @@ -184,7 +175,7 @@ func TestTransactor_WithinTransaction_Nested(t *testing.T) { // Nested transaction (should reuse existing transaction) return transactor.WithinTransaction(outerTxCtx, func(innerTxCtx context.Context) error { - innerModel := TestModel{ + innerModel := TestUser{ Name: "Inner", Email: "inner@example.com", Age: 30, @@ -203,38 +194,44 @@ func TestTransactor_WithinTransaction_Nested(t *testing.T) { assert.NoError(t, err, "WithinTransaction nested should not return error") // Verify both records were committed - outerSpec := crud.Specification[TestModel]{ - Model: TestModel{ID: outerID}, + outerSpec := crud.Specification[TestUser]{ + Model: TestUser{ID: outerID}, } outerResult, err := repo.FindFirst(ctx, outerSpec) assert.NoError(t, err, "Error verifying outer record") assert.NotZero(t, outerResult.ID, "WithinTransaction outer record should be committed") - innerSpec := crud.Specification[TestModel]{ - Model: TestModel{ID: innerID}, + innerSpec := crud.Specification[TestUser]{ + Model: TestUser{ID: innerID}, } innerResult, err := repo.FindFirst(ctx, innerSpec) assert.NoError(t, err, "Error verifying inner record") assert.NotZero(t, innerResult.ID, "WithinTransaction inner record should be committed") } -func TestGetTxFromContext(t *testing.T) { - db := setupTransactorTestDB(t) +func TestTransactor_Begin_Error(t *testing.T) { + // Create a closed database to trigger error + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) + assert.NoError(t, err) + + sqlDB, err := db.DB() + assert.NoError(t, err) + err = sqlDB.Close() + assert.NoError(t, err) + transactor := crud.NewTransactor(db) ctx := context.Background() - t.Run("context with transaction", func(t *testing.T) { - txCtx, err := transactor.Begin(ctx) - assert.NoError(t, err, "Failed to begin transaction") + _, err = transactor.Begin(ctx) + assert.Error(t, err) +} - tx, err := crud.GetTxFromContext(txCtx) - assert.NoError(t, err, "GetTxFromContext should not return error") - assert.NotNil(t, tx, "GetTxFromContext should return transaction") - }) +func TestTransactor_Commit_Error(t *testing.T) { + db := setupTransactorTestDB(t) + transactor := crud.NewTransactor(db) + ctx := context.Background() - t.Run("context without transaction", func(t *testing.T) { - tx, err := crud.GetTxFromContext(ctx) - assert.NoError(t, err, "GetTxFromContext should not return error") - assert.Nil(t, tx, "GetTxFromContext should return nil when no transaction") - }) + // Test committing without a transaction (should not error) + err := transactor.Commit(ctx) + assert.NoError(t, err) // This is expected behavior } diff --git a/test/internal_test.go b/internal/internal_test.go similarity index 99% rename from test/internal_test.go rename to internal/internal_test.go index 4a6f18a..dcf236e 100644 --- a/test/internal_test.go +++ b/internal/internal_test.go @@ -1,4 +1,4 @@ -package gocrud_test +package internal_test import ( "testing" diff --git a/scripts/git-pre-push.sh b/scripts/git-pre-push.sh index 25f732a..c97efaa 100644 --- a/scripts/git-pre-push.sh +++ b/scripts/git-pre-push.sh @@ -1,17 +1,19 @@ #!/bin/sh -# -# Pre-push hook that runs linting and tests before allowing push echo "Running pre-push checks..." -# Run linting echo "\n=== Running linting ===" if ! make lint; then echo "❌ Linting failed! Please fix the issues before pushing." exit 1 fi -# Run tests +echo "\n=== Running build ===" +if ! make build; then + echo "❌ Build failed! Please fix the issues before pushing." + exit 1 +fi + echo "\n=== Running tests ===" if ! make test; then echo "❌ Tests failed! Please fix the test issues before pushing." diff --git a/test/constants_test.go b/test/constants_test.go deleted file mode 100644 index 0f362ef..0000000 --- a/test/constants_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package gocrud_test - -import ( - "testing" - - "github.com/itsLeonB/go-crud/lib" - "github.com/stretchr/testify/assert" -) - -func TestConstants(t *testing.T) { - tests := []struct { - name string - constant interface{} - expected interface{} - }{ - { - name: "ContextKeyGormTx value", - constant: string(lib.ContextKeyGormTx), - expected: "go-crud.gormTx", - }, - { - name: "MsgTransactionError value", - constant: lib.MsgTransactionError, - expected: "error processing transaction", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.expected, tt.constant, "Constant %s should have expected value", tt.name) - }) - } -} - -func TestContextKeyGormTx_Type(t *testing.T) { - // Verify that ContextKeyGormTx is of the correct type - // The constant is defined as txKey type, not string directly - keyStr := string(lib.ContextKeyGormTx) - assert.Equal(t, "go-crud.gormTx", keyStr, "ContextKeyGormTx string value should be correct") -} - -func TestMsgTransactionError_Type(t *testing.T) { - // Verify that MsgTransactionError is a string - assert.IsType(t, "", lib.MsgTransactionError, "MsgTransactionError should be a string") - assert.Equal(t, "error processing transaction", lib.MsgTransactionError, "MsgTransactionError should have correct value") -} - -func TestConstants_NotEmpty(t *testing.T) { - assert.NotEmpty(t, lib.ContextKeyGormTx, "ContextKeyGormTx should not be empty") - assert.NotEmpty(t, lib.MsgTransactionError, "MsgTransactionError should not be empty") -} - -func TestConstants_Uniqueness(t *testing.T) { - constants := map[string]interface{}{ - "ContextKeyGormTx": lib.ContextKeyGormTx, - "MsgTransactionError": lib.MsgTransactionError, - } - - // Check that all constants have different values - values := make(map[interface{}]string) - for name, value := range constants { - if existingName, exists := values[value]; exists { - t.Errorf("Constants %s and %s have the same value: %v", name, existingName, value) - } - values[value] = name - } -} - -func TestContextKeyGormTx_Usage(t *testing.T) { - // Should be usable as a context key (comparable type) - key1 := lib.ContextKeyGormTx - key2 := lib.ContextKeyGormTx - - assert.Equal(t, key1, key2, "ContextKeyGormTx should be comparable and equal to itself") - - // Should be different from other string values - otherKey := "some.other.key" - assert.NotEqual(t, string(key1), otherKey, "ContextKeyGormTx should be unique") -} - -func TestMsgTransactionError_Usage(t *testing.T) { - // Test that the message is suitable for error messages - msg := lib.MsgTransactionError - - assert.GreaterOrEqual(t, len(msg), 5, "MsgTransactionError should be a meaningful message") - - // Should not contain special characters that might break error handling - for _, char := range msg { - assert.True(t, char >= 32 && char <= 126, "MsgTransactionError should not contain non-printable character: %d", char) - } -} diff --git a/test/repository_test.go b/test/repository_test.go deleted file mode 100644 index d333c61..0000000 --- a/test/repository_test.go +++ /dev/null @@ -1,250 +0,0 @@ -package gocrud_test - -import ( - "context" - "testing" - "time" - - crud "github.com/itsLeonB/go-crud" - "github.com/stretchr/testify/assert" - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -// TestModel represents a test entity for CRUD operations -type TestModel struct { - ID uint `gorm:"primaryKey"` - Name string `gorm:"not null"` - Email string `gorm:"unique"` - Age int - CreatedAt time.Time - UpdatedAt time.Time -} - -func setupTestDB(t *testing.T) *gorm.DB { - db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ - Logger: logger.Default.LogMode(logger.Silent), - }) - assert.NoError(t, err, "Failed to connect to test database") - - err = db.AutoMigrate(&TestModel{}) - assert.NoError(t, err, "Failed to migrate test models") - - return db -} - -func TestNewRepository(t *testing.T) { - db := setupTestDB(t) - repo := crud.NewRepository[TestModel](db) - - assert.NotNil(t, repo, "NewRepository should not return nil") -} - -func TestRepository_Insert(t *testing.T) { - db := setupTestDB(t) - repo := crud.NewRepository[TestModel](db) - ctx := context.Background() - - t.Run("successful insert", func(t *testing.T) { - model := TestModel{ - Name: "John Doe", - Email: "john@example.com", - Age: 30, - } - - result, err := repo.Insert(ctx, model) - assert.NoError(t, err, "Insert should not return error") - assert.NotZero(t, result.ID, "Insert should set ID after successful insert") - assert.Equal(t, model.Name, result.Name, "Insert should preserve Name field") - assert.Equal(t, model.Email, result.Email, "Insert should preserve Email field") - assert.Equal(t, model.Age, result.Age, "Insert should preserve Age field") - }) - - t.Run("insert zero value", func(t *testing.T) { - _, err := repo.Insert(ctx, TestModel{}) - assert.Error(t, err, "Insert should return error for zero value") - assert.Contains(t, err.Error(), "zero value", "Error should mention zero value") - }) -} - -func TestRepository_FindAll(t *testing.T) { - db := setupTestDB(t) - repo := crud.NewRepository[TestModel](db) - ctx := context.Background() - - // Insert test data - testModels := []TestModel{ - {Name: "Alice", Email: "alice@example.com", Age: 25}, - {Name: "Bob", Email: "bob@example.com", Age: 30}, - {Name: "Charlie", Email: "charlie@example.com", Age: 35}, - } - - for _, model := range testModels { - _, err := repo.Insert(ctx, model) - assert.NoError(t, err, "Failed to insert test data") - } - - t.Run("find all records", func(t *testing.T) { - results, err := repo.FindAll(ctx, crud.Specification[TestModel]{}) - assert.NoError(t, err, "FindAll should not return error") - assert.Len(t, results, 3, "FindAll should return all 3 records") - }) - - t.Run("find by name", func(t *testing.T) { - spec := crud.Specification[TestModel]{ - Model: TestModel{Name: "Alice"}, - } - results, err := repo.FindAll(ctx, spec) - assert.NoError(t, err, "FindAll should not return error") - assert.Len(t, results, 1, "FindAll should return 1 record for Alice") - assert.Equal(t, "Alice", results[0].Name, "Found record should be Alice") - }) -} - -func TestRepository_FindFirst(t *testing.T) { - db := setupTestDB(t) - repo := crud.NewRepository[TestModel](db) - ctx := context.Background() - - testModel := TestModel{Name: "Alice", Email: "alice@example.com", Age: 25} - _, err := repo.Insert(ctx, testModel) - assert.NoError(t, err, "Failed to insert test data") - - t.Run("find existing record", func(t *testing.T) { - spec := crud.Specification[TestModel]{ - Model: TestModel{Name: "Alice"}, - } - result, err := repo.FindFirst(ctx, spec) - assert.NoError(t, err, "FindFirst should not return error") - assert.NotZero(t, result.ID, "FindFirst should return record with ID when found") - assert.Equal(t, "Alice", result.Name, "Found record should have correct name") - }) - - t.Run("find non-existent record", func(t *testing.T) { - spec := crud.Specification[TestModel]{ - Model: TestModel{Name: "NonExistent"}, - } - result, err := repo.FindFirst(ctx, spec) - assert.NoError(t, err, "FindFirst should not return error for non-existent record") - assert.Zero(t, result.ID, "FindFirst should return zero value when not found") - }) -} - -func TestRepository_Update(t *testing.T) { - db := setupTestDB(t) - repo := crud.NewRepository[TestModel](db) - ctx := context.Background() - - testModel := TestModel{Name: "Alice", Email: "alice@example.com", Age: 25} - inserted, err := repo.Insert(ctx, testModel) - assert.NoError(t, err, "Failed to insert test data") - - t.Run("successful update", func(t *testing.T) { - updated := TestModel{ - ID: inserted.ID, - Name: "Alice Updated", - Email: "alice.updated@example.com", - Age: 26, - } - - result, err := repo.Update(ctx, updated) - assert.NoError(t, err, "Update should not return error") - assert.Equal(t, updated.Name, result.Name, "Update should change Name field") - assert.Equal(t, updated.Email, result.Email, "Update should change Email field") - assert.Equal(t, updated.Age, result.Age, "Update should change Age field") - }) - - t.Run("update zero value", func(t *testing.T) { - _, err := repo.Update(ctx, TestModel{}) - assert.Error(t, err, "Update should return error for zero value") - assert.Contains(t, err.Error(), "zero value", "Error should mention zero value") - }) -} - -func TestRepository_Delete(t *testing.T) { - db := setupTestDB(t) - repo := crud.NewRepository[TestModel](db) - ctx := context.Background() - - testModel := TestModel{Name: "Alice", Email: "alice@example.com", Age: 25} - inserted, err := repo.Insert(ctx, testModel) - assert.NoError(t, err, "Failed to insert test data") - - t.Run("successful delete", func(t *testing.T) { - err := repo.Delete(ctx, inserted) - assert.NoError(t, err, "Delete should not return error") - - // Verify record is deleted - result, err := repo.FindFirst(ctx, crud.Specification[TestModel]{ - Model: TestModel{ID: inserted.ID}, - }) - assert.NoError(t, err, "Error verifying deletion") - assert.Zero(t, result.ID, "Delete should remove the record") - }) - - t.Run("delete zero value", func(t *testing.T) { - err := repo.Delete(ctx, TestModel{}) - assert.Error(t, err, "Delete should return error for zero value") - assert.Contains(t, err.Error(), "zero value", "Error should mention zero value") - }) -} - -func TestRepository_InsertMany(t *testing.T) { - db := setupTestDB(t) - repo := crud.NewRepository[TestModel](db) - ctx := context.Background() - - t.Run("successful batch insert", func(t *testing.T) { - models := []TestModel{ - {Name: "Alice", Email: "alice@example.com", Age: 25}, - {Name: "Bob", Email: "bob@example.com", Age: 30}, - {Name: "Charlie", Email: "charlie@example.com", Age: 35}, - } - - results, err := repo.InsertMany(ctx, models) - assert.NoError(t, err, "InsertMany should not return error") - assert.Len(t, results, len(models), "InsertMany should return same number of records") - - for i, result := range results { - assert.NotZero(t, result.ID, "InsertMany result[%d] should have ID set", i) - assert.Equal(t, models[i].Name, result.Name, "InsertMany result[%d] should preserve Name", i) - assert.Equal(t, models[i].Email, result.Email, "InsertMany result[%d] should preserve Email", i) - assert.Equal(t, models[i].Age, result.Age, "InsertMany result[%d] should preserve Age", i) - } - }) - - t.Run("empty batch insert", func(t *testing.T) { - _, err := repo.InsertMany(ctx, []TestModel{}) - assert.Error(t, err, "InsertMany should return error for empty slice") - assert.Contains(t, err.Error(), "empty", "Error should mention empty") - }) -} - -func TestRepository_GetGormInstance(t *testing.T) { - db := setupTestDB(t) - repo := crud.NewRepository[TestModel](db) - ctx := context.Background() - - instance, err := repo.GetGormInstance(ctx) - assert.NoError(t, err, "GetGormInstance should not return error") - assert.NotNil(t, instance, "GetGormInstance should not return nil instance") -} - -func TestSpecification(t *testing.T) { - spec := crud.Specification[TestModel]{ - Model: TestModel{ - Name: "Alice", - Age: 25, - }, - PreloadRelations: []string{"Profile", "Posts"}, - ForUpdate: true, - } - - assert.Equal(t, "Alice", spec.Model.Name, "Specification should preserve Model.Name") - assert.Equal(t, 25, spec.Model.Age, "Specification should preserve Model.Age") - assert.Len(t, spec.PreloadRelations, 2, "Specification should have 2 PreloadRelations") - assert.Contains(t, spec.PreloadRelations, "Profile", "Specification should contain Profile relation") - assert.Contains(t, spec.PreloadRelations, "Posts", "Specification should contain Posts relation") - assert.True(t, spec.ForUpdate, "Specification.ForUpdate should be true") -} From 48caa3a496d8794409cc967dd26fac353d63107e Mon Sep 17 00:00:00 2001 From: Ellion Blessan Date: Fri, 9 Jan 2026 16:03:38 +0700 Subject: [PATCH 5/6] Update gorm_repository_test.go Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- gorm_repository_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gorm_repository_test.go b/gorm_repository_test.go index d787ffc..3ec9c7c 100644 --- a/gorm_repository_test.go +++ b/gorm_repository_test.go @@ -440,7 +440,7 @@ func TestCRUDRepository_Insert_Error(t *testing.T) { assert.Error(t, err) } -func TestCRUDRepository_Update_Error(t *testing.T) { +func TestCRUDRepository_Update_Success(t *testing.T) { db := setupTestDB() repo := crud.NewRepository[TestUser](db) ctx := context.Background() From 146dcf4e33ae791f755f74c82ae3c912d899e8c3 Mon Sep 17 00:00:00 2001 From: itsLeonB Date: Fri, 9 Jan 2026 16:27:12 +0700 Subject: [PATCH 6/6] test: fix test name --- gorm_transactor_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gorm_transactor_test.go b/gorm_transactor_test.go index 17e2d81..a45c863 100644 --- a/gorm_transactor_test.go +++ b/gorm_transactor_test.go @@ -226,12 +226,12 @@ func TestTransactor_Begin_Error(t *testing.T) { assert.Error(t, err) } -func TestTransactor_Commit_Error(t *testing.T) { +func TestTransactor_Commit_WithoutTransaction(t *testing.T) { db := setupTransactorTestDB(t) transactor := crud.NewTransactor(db) ctx := context.Background() - // Test committing without a transaction (should not error) + // Committing without a transaction should not error err := transactor.Commit(ctx) - assert.NoError(t, err) // This is expected behavior + assert.NoError(t, err, "Commit without transaction should be a no-op") }