tercul-backend/internal/data/sql/base_repository_test.go
google-labs-jules[bot] c2e9a118e2 feat(testing): Increase test coverage and fix authz bugs
This commit significantly increases the test coverage across the application and fixes several underlying bugs that were discovered while writing the new tests.

The key changes include:

- **New Tests:** Added extensive integration and unit tests for GraphQL resolvers, application services, and data repositories, substantially increasing the test coverage for packages like `graphql`, `user`, `translation`, and `analytics`.

- **Authorization Bug Fixes:**
  - Fixed a critical bug where a user creating a `Work` was not correctly associated as its author, causing subsequent permission failures.
  - Corrected the authorization logic in `authz.Service` to properly check for entity ownership by non-admin users.

- **Test Refactoring:**
  - Refactored numerous test suites to use `testify/mock` instead of manual mocks, improving test clarity and maintainability.
  - Isolated integration tests by creating a fresh admin user and token for each test run, eliminating test pollution.
  - Centralized domain errors into `internal/domain/errors.go` and updated repositories to use them, making error handling more consistent.

- **Code Quality Improvements:**
  - Replaced manual mock implementations with `testify/mock` for better consistency.
  - Cleaned up redundant and outdated test files.

These changes stabilize the test suite, improve the overall quality of the codebase, and move the project closer to the goal of 80% test coverage.
2025-10-09 07:03:45 +00:00

267 lines
7.1 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.IntegrationTestSuite.SetupTest()
s.DB.Exec("DELETE FROM test_entities")
}
// TearDownSuite drops the test table after the suite finishes.
func (s *BaseRepositoryTestSuite) TearDownSuite() {
err := s.DB.Migrator().DropTable(&testutil.TestEntity{})
s.Require().NoError(err)
}
// 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, domain.ErrValidation)
})
s.Run("should return error for nil context", func() {
//nolint:staticcheck // Testing behavior with nil context is intentional here.
err := s.repo.Create(nil, &testutil.TestEntity{Name: "Test Context"})
s.ErrorIs(err, domain.ErrValidation)
})
}
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, domain.ErrEntityNotFound)
})
s.Run("should return ErrValidation for zero ID", func() {
_, err := s.repo.GetByID(context.Background(), 0)
s.ErrorIs(err, domain.ErrValidation)
})
}
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, domain.ErrEntityNotFound)
})
s.Run("should return ErrEntityNotFound when deleting non-existent entity", func() {
err := s.repo.Delete(context.Background(), 99999)
s.ErrorIs(err, domain.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, domain.ErrEntityNotFound, "Entity should not exist after rollback")
})
}