diff --git a/Makefile b/Makefile index b7909e0..06b6411 100644 --- a/Makefile +++ b/Makefile @@ -1,50 +1,76 @@ -.PHONY: - help - lint - test-all - test-verbose - test-coverage - test-coverage-html - test-clean - install-pre-push-hook - uninstall-pre-push-hook +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 help: - @echo "Available commands:" - @echo " help - Show this help message" - @echo " lint - Run golangci-lint on the codebase" - @echo " test - Run all tests" - @echo " test-verbose - Run all tests with verbose output" - @echo " test-coverage - Run all tests with coverage report" - @echo " test-coverage-html - Run all tests and generate HTML coverage report" - @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 "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" lint: golangci-lint run ./... -test-all: +test: @echo "Running all tests..." - go test ./test/... + @if [ -d $(TEST_DIR) ]; then \ + go test $(TEST_DIR)/...; \ + else \ + echo "No tests found in $(TEST_DIR), skipping."; \ + fi test-verbose: @echo "Running all tests with verbose output..." - go test -v ./test/... + @if [ -d $(TEST_DIR) ]; then \ + go test -v $(TEST_DIR)/...; \ + else \ + echo "No tests found in $(TEST_DIR), skipping."; \ + fi test-coverage: @echo "Running all tests with coverage report..." - go test -v -cover -coverprofile=coverage.out -coverpkg=./... ./test/... + @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 test-coverage-html: @echo "Running all tests and generating HTML coverage report..." - go test -v -cover -coverprofile=coverage.out -coverpkg=./... ./test/... - go tool cover -html=coverage.out -o coverage.html - @echo "Coverage report generated: coverage.html" + @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 test-clean: @echo "Cleaning test cache and running tests..." - go clean -testcache && go test -v ./test/... + @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 install-pre-push-hook: @echo "Installing pre-push git hook..." diff --git a/go.mod b/go.mod index 4e3b767..75fb6fa 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/itsLeonB/ezutil/v2 v2.0.0 github.com/rotisserie/eris v0.5.4 github.com/stretchr/testify v1.11.0 + go.uber.org/mock v0.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.30.1 ) diff --git a/go.sum b/go.sum index fc9267d..2e98e00 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ 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= +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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/gorm_repository.go b/gorm_repository.go index 3d0327e..5952e20 100644 --- a/gorm_repository.go +++ b/gorm_repository.go @@ -22,8 +22,10 @@ type Repository[T any] interface { Update(ctx context.Context, model T) (T, error) // Delete removes a record from the database (hard delete). Delete(ctx context.Context, model T) error - // BatchInsert creates multiple records in a single database operation. - BatchInsert(ctx context.Context, models []T) ([]T, error) + // InsertMany creates multiple records in a single database operation. + InsertMany(ctx context.Context, models []T) ([]T, error) + // DeleteMany removes multiple records in a single database operation (hard delete). + DeleteMany(ctx context.Context, models []T) error // GetGormInstance returns the appropriate GORM DB instance (transaction-aware). GetGormInstance(ctx context.Context) (*gorm.DB, error) } @@ -159,7 +161,7 @@ func (gr *gormRepository[T]) Delete(ctx context.Context, model T) error { return nil } -func (gr *gormRepository[T]) BatchInsert(ctx context.Context, models []T) ([]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") } @@ -176,6 +178,23 @@ func (gr *gormRepository[T]) BatchInsert(ctx context.Context, models []T) ([]T, return models, nil } +func (gr *gormRepository[T]) DeleteMany(ctx context.Context, models []T) error { + if len(models) < 1 { + return eris.Errorf("deleted models cannot be empty") + } + + db, err := gr.GetGormInstance(ctx) + if err != nil { + return err + } + + if err = db.Unscoped().Delete(&models).Error; err != nil { + return eris.Wrap(err, "error batch deleting data") + } + + return nil +} + func (gr *gormRepository[T]) checkZeroValue(model T) error { if reflect.DeepEqual(model, *new(T)) { return eris.New("model cannot be zero value") diff --git a/gorm_repository_mock.go b/gorm_repository_mock.go new file mode 100644 index 0000000..0b77379 --- /dev/null +++ b/gorm_repository_mock.go @@ -0,0 +1,160 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./gorm_repository.go +// +// Generated by this command: +// +// mockgen -source=./gorm_repository.go -destination=./gorm_repository_mock.go -package=crud +// + +// Package crud is a generated GoMock package. +package crud + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + gorm "gorm.io/gorm" +) + +// MockRepository is a mock of Repository interface. +type MockRepository[T any] struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder[T] + isgomock struct{} +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder[T any] struct { + mock *MockRepository[T] +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository[T any](ctrl *gomock.Controller) *MockRepository[T] { + mock := &MockRepository[T]{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder[T]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository[T]) EXPECT() *MockRepositoryMockRecorder[T] { + return m.recorder +} + +// Delete mocks base method. +func (m *MockRepository[T]) Delete(ctx context.Context, model T) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, model) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockRepositoryMockRecorder[T]) Delete(ctx, model any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRepository[T])(nil).Delete), ctx, model) +} + +// DeleteMany mocks base method. +func (m *MockRepository[T]) DeleteMany(ctx context.Context, models []T) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteMany", ctx, models) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteMany indicates an expected call of DeleteMany. +func (mr *MockRepositoryMockRecorder[T]) DeleteMany(ctx, models any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMany", reflect.TypeOf((*MockRepository[T])(nil).DeleteMany), ctx, models) +} + +// FindAll mocks base method. +func (m *MockRepository[T]) FindAll(ctx context.Context, spec Specification[T]) ([]T, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindAll", ctx, spec) + ret0, _ := ret[0].([]T) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindAll indicates an expected call of FindAll. +func (mr *MockRepositoryMockRecorder[T]) FindAll(ctx, spec any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindAll", reflect.TypeOf((*MockRepository[T])(nil).FindAll), ctx, spec) +} + +// FindFirst mocks base method. +func (m *MockRepository[T]) FindFirst(ctx context.Context, spec Specification[T]) (T, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindFirst", ctx, spec) + ret0, _ := ret[0].(T) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindFirst indicates an expected call of FindFirst. +func (mr *MockRepositoryMockRecorder[T]) FindFirst(ctx, spec any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindFirst", reflect.TypeOf((*MockRepository[T])(nil).FindFirst), ctx, spec) +} + +// GetGormInstance mocks base method. +func (m *MockRepository[T]) GetGormInstance(ctx context.Context) (*gorm.DB, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetGormInstance", ctx) + ret0, _ := ret[0].(*gorm.DB) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetGormInstance indicates an expected call of GetGormInstance. +func (mr *MockRepositoryMockRecorder[T]) GetGormInstance(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGormInstance", reflect.TypeOf((*MockRepository[T])(nil).GetGormInstance), ctx) +} + +// Insert mocks base method. +func (m *MockRepository[T]) Insert(ctx context.Context, model T) (T, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Insert", ctx, model) + ret0, _ := ret[0].(T) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Insert indicates an expected call of Insert. +func (mr *MockRepositoryMockRecorder[T]) Insert(ctx, model any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockRepository[T])(nil).Insert), ctx, model) +} + +// InsertMany mocks base method. +func (m *MockRepository[T]) InsertMany(ctx context.Context, models []T) ([]T, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InsertMany", ctx, models) + ret0, _ := ret[0].([]T) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// InsertMany indicates an expected call of InsertMany. +func (mr *MockRepositoryMockRecorder[T]) InsertMany(ctx, models any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMany", reflect.TypeOf((*MockRepository[T])(nil).InsertMany), ctx, models) +} + +// Update mocks base method. +func (m *MockRepository[T]) Update(ctx context.Context, model T) (T, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, model) + ret0, _ := ret[0].(T) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockRepositoryMockRecorder[T]) Update(ctx, model any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRepository[T])(nil).Update), ctx, model) +} diff --git a/gorm_transactor_mock.go b/gorm_transactor_mock.go new file mode 100644 index 0000000..8fef1c3 --- /dev/null +++ b/gorm_transactor_mock.go @@ -0,0 +1,96 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./gorm_transactor.go +// +// Generated by this command: +// +// mockgen -source=./gorm_transactor.go -destination=./gorm_transactor_mock.go -package=crud +// + +// Package crud is a generated GoMock package. +package crud + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockTransactor is a mock of Transactor interface. +type MockTransactor struct { + ctrl *gomock.Controller + recorder *MockTransactorMockRecorder + isgomock struct{} +} + +// MockTransactorMockRecorder is the mock recorder for MockTransactor. +type MockTransactorMockRecorder struct { + mock *MockTransactor +} + +// NewMockTransactor creates a new mock instance. +func NewMockTransactor(ctrl *gomock.Controller) *MockTransactor { + mock := &MockTransactor{ctrl: ctrl} + mock.recorder = &MockTransactorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTransactor) EXPECT() *MockTransactorMockRecorder { + return m.recorder +} + +// Begin mocks base method. +func (m *MockTransactor) Begin(ctx context.Context) (context.Context, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Begin", ctx) + ret0, _ := ret[0].(context.Context) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Begin indicates an expected call of Begin. +func (mr *MockTransactorMockRecorder) Begin(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Begin", reflect.TypeOf((*MockTransactor)(nil).Begin), ctx) +} + +// Commit mocks base method. +func (m *MockTransactor) Commit(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Commit indicates an expected call of Commit. +func (mr *MockTransactorMockRecorder) Commit(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockTransactor)(nil).Commit), ctx) +} + +// Rollback mocks base method. +func (m *MockTransactor) Rollback(ctx context.Context) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Rollback", ctx) +} + +// Rollback indicates an expected call of Rollback. +func (mr *MockTransactorMockRecorder) Rollback(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockTransactor)(nil).Rollback), ctx) +} + +// WithinTransaction mocks base method. +func (m *MockTransactor) WithinTransaction(ctx context.Context, serviceFn func(context.Context) error) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithinTransaction", ctx, serviceFn) + ret0, _ := ret[0].(error) + return ret0 +} + +// WithinTransaction indicates an expected call of WithinTransaction. +func (mr *MockTransactorMockRecorder) WithinTransaction(ctx, serviceFn any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithinTransaction", reflect.TypeOf((*MockTransactor)(nil).WithinTransaction), ctx, serviceFn) +} diff --git a/scripts/gen-mocks.sh b/scripts/gen-mocks.sh new file mode 100755 index 0000000..900ec60 --- /dev/null +++ b/scripts/gen-mocks.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Script to generate gomock mocks for each file with interfaces +# Only searches root level (current directory only) +# Creates _mock.go next to each source file + +set -e + +echo "Scanning for interfaces in root directory... +" + +# Counter for files processed +count=0 +success=0 +failed=0 + +# Find all Go files in root directory only (maxdepth 1) +find . -maxdepth 1 -name "*.go" \ + -not -name "*_test.go" \ + -not -name "*_mock.go" \ + -type f | while read -r file; do + + # Check if file contains interface definitions + if grep -q "type.*interface\s*{" "$file"; then + count=$((count + 1)) + + # Get base filename + base=$(basename "$file" .go) + output="./${base}_mock.go" + + echo "Processing: $file" + + # Get package name from the file + package=$(grep -m 1 "^package " "$file" | awk '{print $2}') + + # Generate mock file + if GO111MODULE=on mockgen -source="$file" -destination="$output" -package="$package" 2>/dev/null; then + echo " ✓ Generated: $output" + success=$((success + 1)) + else + echo " ✗ Failed to generate mock for: $file" + failed=$((failed + 1)) + fi + fi +done + +echo "" +echo "======================================" +echo "Mock generation complete!" +echo "Files with interfaces found and processed" +echo "Check *_mock.go files in current directory" +echo "======================================" diff --git a/scripts/git-pre-push.sh b/scripts/git-pre-push.sh index 1c6b6f2..25f732a 100644 --- a/scripts/git-pre-push.sh +++ b/scripts/git-pre-push.sh @@ -13,7 +13,7 @@ fi # Run tests echo "\n=== Running tests ===" -if ! make test-all; then +if ! make test; then echo "❌ Tests failed! Please fix the test issues before pushing." exit 1 fi diff --git a/test/repository_test.go b/test/repository_test.go index a7be787..d333c61 100644 --- a/test/repository_test.go +++ b/test/repository_test.go @@ -190,7 +190,7 @@ func TestRepository_Delete(t *testing.T) { }) } -func TestRepository_BatchInsert(t *testing.T) { +func TestRepository_InsertMany(t *testing.T) { db := setupTestDB(t) repo := crud.NewRepository[TestModel](db) ctx := context.Background() @@ -202,21 +202,21 @@ func TestRepository_BatchInsert(t *testing.T) { {Name: "Charlie", Email: "charlie@example.com", Age: 35}, } - results, err := repo.BatchInsert(ctx, models) - assert.NoError(t, err, "BatchInsert should not return error") - assert.Len(t, results, len(models), "BatchInsert should return same number of records") + 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, "BatchInsert result[%d] should have ID set", i) - assert.Equal(t, models[i].Name, result.Name, "BatchInsert result[%d] should preserve Name", i) - assert.Equal(t, models[i].Email, result.Email, "BatchInsert result[%d] should preserve Email", i) - assert.Equal(t, models[i].Age, result.Age, "BatchInsert result[%d] should preserve Age", i) + 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.BatchInsert(ctx, []TestModel{}) - assert.Error(t, err, "BatchInsert should return error for empty slice") + _, 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") }) }