package sql_test import ( "context" "errors" "testing" "tercul/internal/data/sql" "tercul/internal/domain" "tercul/internal/platform/config" "tercul/internal/testutil" "github.com/stretchr/testify/suite" "gorm.io/gorm" ) // BaseRepositoryTestSuite tests the generic BaseRepository implementation. type BaseRepositoryTestSuite struct { testutil.IntegrationTestSuite repo domain.BaseRepository[testutil.TestEntity] cfg *config.Config } // SetupSuite initializes the test suite, database, and repository. func (s *BaseRepositoryTestSuite) SetupSuite() { s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) cfg, err := config.LoadConfig() s.Require().NoError(err) s.cfg = cfg s.repo = sql.NewBaseRepositoryImpl[testutil.TestEntity](s.DB, s.cfg) } // SetupTest cleans the database before each test. func (s *BaseRepositoryTestSuite) SetupTest() { s.DB.Exec("DELETE FROM test_entities") } // TearDownSuite drops the test table after the suite finishes. func (s *BaseRepositoryTestSuite) TearDownSuite() { s.DB.Migrator().DropTable(&testutil.TestEntity{}) } // TestBaseRepository runs the entire test suite. func TestBaseRepository(t *testing.T) { suite.Run(t, new(BaseRepositoryTestSuite)) } // createTestEntity is a helper to create a test entity. func (s *BaseRepositoryTestSuite) createTestEntity(name string) *testutil.TestEntity { entity := &testutil.TestEntity{Name: name} err := s.repo.Create(context.Background(), entity) s.Require().NoError(err) s.Require().NotZero(entity.ID) return entity } func (s *BaseRepositoryTestSuite) TestCreate() { s.Run("should create a new entity", func() { // Arrange ctx := context.Background() entity := &testutil.TestEntity{Name: "Test Create"} // Act err := s.repo.Create(ctx, entity) // Assert s.Require().NoError(err) s.NotZero(entity.ID) // Verify in DB var foundEntity testutil.TestEntity err = s.DB.First(&foundEntity, entity.ID).Error s.Require().NoError(err) s.Equal("Test Create", foundEntity.Name) }) s.Run("should return error for nil entity", func() { err := s.repo.Create(context.Background(), nil) s.ErrorIs(err, sql.ErrInvalidInput) }) s.Run("should return error for nil context", func() { err := s.repo.Create(nil, &testutil.TestEntity{Name: "Test Context"}) s.ErrorIs(err, sql.ErrContextRequired) }) } func (s *BaseRepositoryTestSuite) TestGetByID() { s.Run("should return an entity by ID", func() { // Arrange created := s.createTestEntity("Test GetByID") // Act found, err := s.repo.GetByID(context.Background(), created.ID) // Assert s.Require().NoError(err) s.Require().NotNil(found) s.Equal(created.ID, found.ID) s.Equal(created.Name, found.Name) }) s.Run("should return ErrEntityNotFound for non-existent ID", func() { _, err := s.repo.GetByID(context.Background(), 99999) s.ErrorIs(err, sql.ErrEntityNotFound) }) s.Run("should return ErrInvalidID for zero ID", func() { _, err := s.repo.GetByID(context.Background(), 0) s.ErrorIs(err, sql.ErrInvalidID) }) } func (s *BaseRepositoryTestSuite) TestUpdate() { s.Run("should update an existing entity", func() { // Arrange created := s.createTestEntity("Original Name") created.Name = "Updated Name" // Act err := s.repo.Update(context.Background(), created) // Assert s.Require().NoError(err) found, getErr := s.repo.GetByID(context.Background(), created.ID) s.Require().NoError(getErr) s.Equal("Updated Name", found.Name) }) } func (s *BaseRepositoryTestSuite) TestDelete() { s.Run("should delete an existing entity", func() { // Arrange created := s.createTestEntity("To Be Deleted") // Act err := s.repo.Delete(context.Background(), created.ID) // Assert s.Require().NoError(err) _, getErr := s.repo.GetByID(context.Background(), created.ID) s.ErrorIs(getErr, sql.ErrEntityNotFound) }) s.Run("should return ErrEntityNotFound when deleting non-existent entity", func() { err := s.repo.Delete(context.Background(), 99999) s.ErrorIs(err, sql.ErrEntityNotFound) }) } func (s *BaseRepositoryTestSuite) TestList() { // Arrange s.createTestEntity("Entity 1") s.createTestEntity("Entity 2") s.createTestEntity("Entity 3") s.Run("should return a paginated list of entities", func() { // Act result, err := s.repo.List(context.Background(), 1, 2) // Assert s.Require().NoError(err) s.Equal(int64(3), result.TotalCount) s.Equal(2, result.TotalPages) s.Equal(1, result.Page) s.Equal(2, result.PageSize) s.True(result.HasNext) s.False(result.HasPrev) s.Len(result.Items, 2) }) } func (s *BaseRepositoryTestSuite) TestListWithOptions() { // Arrange s.createTestEntity("Apple") s.createTestEntity("Banana") s.createTestEntity("Avocado") s.Run("should filter with Where clause", func() { // Act options := &domain.QueryOptions{ Where: map[string]interface{}{"name LIKE ?": "A%"}, } results, err := s.repo.ListWithOptions(context.Background(), options) // Assert s.Require().NoError(err) s.Len(results, 2) }) s.Run("should order results", func() { // Act options := &domain.QueryOptions{OrderBy: "name desc"} results, err := s.repo.ListWithOptions(context.Background(), options) // Assert s.Require().NoError(err) s.Len(results, 3) s.Equal("Banana", results[0].Name) s.Equal("Avocado", results[1].Name) s.Equal("Apple", results[2].Name) }) } func (s *BaseRepositoryTestSuite) TestCount() { // Arrange s.createTestEntity("Entity 1") s.createTestEntity("Entity 2") s.Run("should return the total count of entities", func() { // Act count, err := s.repo.Count(context.Background()) // Assert s.Require().NoError(err) s.Equal(int64(2), count) }) } func (s *BaseRepositoryTestSuite) TestWithTx() { s.Run("should commit transaction on success", func() { // Arrange var createdID uint // Act err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error { entity := &testutil.TestEntity{Name: "TX Commit"} repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg) if err := repoInTx.Create(context.Background(), entity); err != nil { return err } createdID = entity.ID return nil }) // Assert s.Require().NoError(err) _, getErr := s.repo.GetByID(context.Background(), createdID) s.NoError(getErr, "Entity should exist after commit") }) s.Run("should rollback transaction on error", func() { // Arrange var createdID uint simulatedErr := errors.New("simulated error") // Act err := s.repo.WithTx(context.Background(), func(tx *gorm.DB) error { entity := &testutil.TestEntity{Name: "TX Rollback"} repoInTx := sql.NewBaseRepositoryImpl[testutil.TestEntity](tx, s.cfg) if err := repoInTx.Create(context.Background(), entity); err != nil { return err } createdID = entity.ID return simulatedErr // Force a rollback }) // Assert s.Require().Error(err) s.ErrorIs(err, simulatedErr) _, getErr := s.repo.GetByID(context.Background(), createdID) s.ErrorIs(getErr, sql.ErrEntityNotFound, "Entity should not exist after rollback") }) }