tercul-backend/internal/data/sql/base_repository_test.go
google-labs-jules[bot] a8dfb727a1 feat: Implement critical features and fix build
This commit addresses several high-priority tasks from the TASKS.md file, including:

- **Fix Background Job Panic:** Replaced `log.Fatalf` with `log.Printf` in the `asynq` server to prevent crashes.
- **Refactor API Server Setup:** Consolidated the GraphQL Playground and Prometheus metrics endpoints into the main API server.
- **Implement `DeleteUser` Mutation:** Implemented the `DeleteUser` resolver.
- **Implement `CreateContribution` Mutation:** Implemented the `CreateContribution` resolver and its required application service.

Additionally, this commit includes a major refactoring of the configuration management system to fix a broken build. The global `config.Cfg` variable has been removed and replaced with a dependency injection approach, where the configuration object is passed to all components that require it. This change has been applied across the entire codebase, including the test suite, to ensure a stable and testable application.
2025-10-05 18:29:18 +00:00

264 lines
6.9 KiB
Go

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")
})
}