package translation_test import ( "context" "testing" "gorm.io/gorm" "tercul/internal/app/authz" "tercul/internal/app/translation" "tercul/internal/domain" platform_auth "tercul/internal/platform/auth" "tercul/internal/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) // MockAuthorRepository is a mock implementation of the AuthorRepository interface. type mockAuthorRepository struct{ mock.Mock } func (m *mockAuthorRepository) Create(ctx context.Context, entity *domain.Author) error { args := m.Called(ctx, entity) return args.Error(0) } func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { args := m.Called(ctx, tx, entity) return args.Error(0) } func (m *mockAuthorRepository) GetByID(ctx context.Context, id uuid.UUID) (*domain.Author, error) { args := m.Called(ctx, id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Author), args.Error(1) } func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { args := m.Called(ctx, id, options) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Author), args.Error(1) } func (m *mockAuthorRepository) Update(ctx context.Context, entity *domain.Author) error { args := m.Called(ctx, entity) return args.Error(0) } func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { args := m.Called(ctx, tx, entity) return args.Error(0) } func (m *mockAuthorRepository) Delete(ctx context.Context, id uint) error { args := m.Called(ctx, id) return args.Error(0) } func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { args := m.Called(ctx, tx, id) return args.Error(0) } func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) { args := m.Called(ctx, page, pageSize) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.PaginatedResult[domain.Author]), args.Error(1) } func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) { args := m.Called(ctx, options) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]domain.Author), args.Error(1) } func (m *mockAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) { args := m.Called(ctx) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]domain.Author), args.Error(1) } func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) { args := m.Called(ctx) return args.Get(0).(int64), args.Error(1) } func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { args := m.Called(ctx, options) return args.Get(0).(int64), args.Error(1) } func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) { args := m.Called(ctx, preloads, id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Author), args.Error(1) } func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) { args := m.Called(ctx, batchSize, offset) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]domain.Author), args.Error(1) } func (m *mockAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) { args := m.Called(ctx, id) return args.Bool(0), args.Error(1) } func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { args := m.Called(ctx) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*gorm.DB), args.Error(1) } func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return fn(nil) } func (m *mockAuthorRepository) FindByName(ctx context.Context, name string) (*domain.Author, error) { args := m.Called(ctx, name) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Author), args.Error(1) } func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) { args := m.Called(ctx, workID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]domain.Author), args.Error(1) } func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) { args := m.Called(ctx, bookID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]domain.Author), args.Error(1) } func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) { args := m.Called(ctx, countryID) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).([]domain.Author), args.Error(1) } func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) { args := m.Called(ctx, id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*domain.Author), args.Error(1) } type TranslationCommandsTestSuite struct { suite.Suite mockWorkRepo *testutil.MockWorkRepository mockTranslationRepo *testutil.MockTranslationRepository mockAuthorRepo *mockAuthorRepository mockUserRepo *testutil.MockUserRepository authzSvc *authz.Service cmd *translation.TranslationCommands adminCtx context.Context userCtx context.Context adminUser *domain.User regularUser *domain.User } func (s *TranslationCommandsTestSuite) SetupTest() { s.mockWorkRepo = new(testutil.MockWorkRepository) s.mockTranslationRepo = new(testutil.MockTranslationRepository) s.mockAuthorRepo = new(mockAuthorRepository) s.mockUserRepo = new(testutil.MockUserRepository) s.authzSvc = authz.NewService(s.mockWorkRepo, s.mockAuthorRepo, s.mockUserRepo, s.mockTranslationRepo) s.cmd = translation.NewTranslationCommands(s.mockTranslationRepo, s.authzSvc) s.adminUser = &domain.User{BaseModel: domain.BaseModel{ID: 1}, Role: domain.UserRoleAdmin, Username: "admin"} s.regularUser = &domain.User{BaseModel: domain.BaseModel{ID: 2}, Role: domain.UserRoleContributor, Username: "contributor"} s.adminCtx = context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{ UserID: s.adminUser.ID, Role: string(s.adminUser.Role), }) s.userCtx = context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{ UserID: s.regularUser.ID, Role: string(s.regularUser.Role), }) } func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() { testWork := &domain.Work{ TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, } testAuthor := &domain.Author{ TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Name: s.regularUser.Username, } baseInput := translation.CreateOrUpdateTranslationInput{ Title: "Test Title", Content: "Test content", Language: "es", TranslatableID: testWork.ID, TranslatableType: "works", } s.Run("should create translation for admin", func() { s.SetupTest() input := baseInput // Arrange s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once() s.mockTranslationRepo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.Translation")).Return(nil).Once() // Act result, err := s.cmd.CreateOrUpdateTranslation(s.adminCtx, input) // Assert s.NoError(err) s.NotNil(result) s.Equal(input.Title, result.Title) s.Equal(s.adminUser.ID, *result.TranslatorID) s.mockWorkRepo.AssertExpectations(s.T()) s.mockTranslationRepo.AssertExpectations(s.T()) }) s.Run("should create translation for author", func() { s.SetupTest() input := baseInput // Arrange s.mockUserRepo.On("GetByID", mock.Anything, s.regularUser.ID).Return(s.regularUser, nil).Once() s.mockAuthorRepo.On("FindByName", mock.Anything, s.regularUser.Username).Return(testAuthor, nil).Once() s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once() s.mockWorkRepo.On("IsAuthor", mock.Anything, testWork.ID, testAuthor.ID).Return(true, nil).Once() s.mockTranslationRepo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.Translation")).Return(nil).Once() // Act result, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, input) // Assert s.NoError(err) s.NotNil(result) s.Equal(input.Title, result.Title) s.Equal(s.regularUser.ID, *result.TranslatorID) s.mockUserRepo.AssertExpectations(s.T()) s.mockAuthorRepo.AssertExpectations(s.T()) s.mockWorkRepo.AssertExpectations(s.T()) s.mockTranslationRepo.AssertExpectations(s.T()) }) s.Run("should fail if user is not authorized", func() { s.SetupTest() input := baseInput // Arrange s.mockUserRepo.On("GetByID", mock.Anything, s.regularUser.ID).Return(s.regularUser, nil).Once() s.mockAuthorRepo.On("FindByName", mock.Anything, s.regularUser.Username).Return(testAuthor, nil).Once() s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once() s.mockWorkRepo.On("IsAuthor", mock.Anything, testWork.ID, testAuthor.ID).Return(false, nil).Once() // Act _, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, input) // Assert s.Error(err) s.ErrorIs(err, domain.ErrForbidden) s.mockUserRepo.AssertExpectations(s.T()) s.mockAuthorRepo.AssertExpectations(s.T()) s.mockWorkRepo.AssertExpectations(s.T()) }) s.Run("should fail on validation error for empty language", func() { s.SetupTest() // Arrange input := baseInput input.Language = "" // Act _, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, input) // Assert s.Error(err) assert.ErrorContains(s.T(), err, "language cannot be empty") }) } func TestTranslationCommands(t *testing.T) { suite.Run(t, new(TranslationCommandsTestSuite)) }