package work import ( "context" "errors" "testing" "tercul/internal/platform/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "tercul/internal/app/authz" "tercul/internal/data/sql" "tercul/internal/domain" platform_auth "tercul/internal/platform/auth" ) type WorkCommandsSuite struct { suite.Suite repo *mockWorkRepository searchClient *mockSearchClient authzSvc *authz.Service commands *WorkCommands } func (s *WorkCommandsSuite) SetupTest() { s.repo = &mockWorkRepository{} s.searchClient = &mockSearchClient{} s.authzSvc = authz.NewService(s.repo, nil) s.commands = NewWorkCommands(s.repo, s.searchClient, s.authzSvc) } func TestWorkCommandsSuite(t *testing.T) { suite.Run(t, new(WorkCommandsSuite)) } func (s *WorkCommandsSuite) TestCreateWork_Success() { work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} _, err := s.commands.CreateWork(context.Background(), work) assert.NoError(s.T(), err) } func (s *WorkCommandsSuite) TestCreateWork_Nil() { _, err := s.commands.CreateWork(context.Background(), nil) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestCreateWork_EmptyTitle() { work := &domain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}} _, err := s.commands.CreateWork(context.Background(), work) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestCreateWork_EmptyLanguage() { work := &domain.Work{Title: "Test Work"} _, err := s.commands.CreateWork(context.Background(), work) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestCreateWork_RepoError() { work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} s.repo.createFunc = func(ctx context.Context, w *domain.Work) error { return errors.New("db error") } _, err := s.commands.CreateWork(context.Background(), work) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestUpdateWork_Success() { ctx := platform_auth.ContextWithAdminUser(context.Background(), 1) work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} work.ID = 1 s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Work, error) { return work, nil } s.repo.isAuthorFunc = func(ctx context.Context, workID uint, authorID uint) (bool, error) { return true, nil } err := s.commands.UpdateWork(ctx, work) assert.NoError(s.T(), err) } func (s *WorkCommandsSuite) TestUpdateWork_Nil() { err := s.commands.UpdateWork(context.Background(), nil) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestUpdateWork_ZeroID() { work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} err := s.commands.UpdateWork(context.Background(), work) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestUpdateWork_EmptyTitle() { work := &domain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}} work.ID = 1 err := s.commands.UpdateWork(context.Background(), work) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestUpdateWork_EmptyLanguage() { work := &domain.Work{Title: "Test Work"} work.ID = 1 err := s.commands.UpdateWork(context.Background(), work) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestUpdateWork_RepoError() { work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} work.ID = 1 s.repo.updateFunc = func(ctx context.Context, w *domain.Work) error { return errors.New("db error") } err := s.commands.UpdateWork(context.Background(), work) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestDeleteWork_Success() { ctx := platform_auth.ContextWithAdminUser(context.Background(), 1) work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} work.ID = 1 s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Work, error) { return work, nil } s.repo.isAuthorFunc = func(ctx context.Context, workID uint, authorID uint) (bool, error) { return true, nil } err := s.commands.DeleteWork(ctx, 1) assert.NoError(s.T(), err) } func (s *WorkCommandsSuite) TestDeleteWork_ZeroID() { err := s.commands.DeleteWork(context.Background(), 0) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestDeleteWork_RepoError() { s.repo.deleteFunc = func(ctx context.Context, id uint) error { return errors.New("db error") } err := s.commands.DeleteWork(context.Background(), 1) assert.Error(s.T(), err) } func (s *WorkCommandsSuite) TestAnalyzeWork_Success() { err := s.commands.AnalyzeWork(context.Background(), 1) assert.NoError(s.T(), err) } func TestMergeWork_Integration(t *testing.T) { // Setup in-memory SQLite DB db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) assert.NoError(t, err) // Run migrations for all relevant tables err = db.AutoMigrate( &domain.Work{}, &domain.Translation{}, &domain.Author{}, &domain.Tag{}, &domain.Category{}, &domain.Copyright{}, &domain.Monetization{}, &domain.WorkStats{}, &domain.WorkAuthor{}, ) assert.NoError(t, err) // Create real repositories and services pointing to the test DB cfg, err := config.LoadConfig() assert.NoError(t, err) workRepo := sql.NewWorkRepository(db, cfg) authzSvc := authz.NewService(workRepo, nil) // Using real repo for authz checks searchClient := &mockSearchClient{} // Mock search client is fine commands := NewWorkCommands(workRepo, searchClient, authzSvc) // --- Seed Data --- author1 := &domain.Author{Name: "Author One"} db.Create(author1) author2 := &domain.Author{Name: "Author Two"} db.Create(author2) tag1 := &domain.Tag{Name: "Tag One"} db.Create(tag1) tag2 := &domain.Tag{Name: "Tag Two"} db.Create(tag2) sourceWork := &domain.Work{ TranslatableModel: domain.TranslatableModel{Language: "en"}, Title: "Source Work", Authors: []*domain.Author{author1}, Tags: []*domain.Tag{tag1}, } db.Create(sourceWork) db.Create(&domain.Translation{Title: "Source English", Language: "en", TranslatableID: sourceWork.ID, TranslatableType: "works"}) db.Create(&domain.Translation{Title: "Source French", Language: "fr", TranslatableID: sourceWork.ID, TranslatableType: "works"}) db.Create(&domain.WorkStats{WorkID: sourceWork.ID, Views: 10, Likes: 5}) targetWork := &domain.Work{ TranslatableModel: domain.TranslatableModel{Language: "en"}, Title: "Target Work", Authors: []*domain.Author{author2}, Tags: []*domain.Tag{tag2}, } db.Create(targetWork) db.Create(&domain.Translation{Title: "Target English", Language: "en", TranslatableID: targetWork.ID, TranslatableType: "works"}) db.Create(&domain.WorkStats{WorkID: targetWork.ID, Views: 20, Likes: 10}) // --- Execute Merge --- ctx := platform_auth.ContextWithAdminUser(context.Background(), 1) err = commands.MergeWork(ctx, sourceWork.ID, targetWork.ID) assert.NoError(t, err) // --- Assertions --- // 1. Source work should be deleted var deletedWork domain.Work err = db.First(&deletedWork, sourceWork.ID).Error assert.Error(t, err) assert.True(t, errors.Is(err, gorm.ErrRecordNotFound)) // 2. Target work should have merged data var finalTargetWork domain.Work db.Preload("Translations").Preload("Authors").Preload("Tags").First(&finalTargetWork, targetWork.ID) assert.Len(t, finalTargetWork.Translations, 2, "Should have two translations after merge") foundEn := false foundFr := false for _, tr := range finalTargetWork.Translations { if tr.Language == "en" { foundEn = true assert.Equal(t, "Target English", tr.Title, "Should keep target's English translation") } if tr.Language == "fr" { foundFr = true assert.Equal(t, "Source French", tr.Title, "Should merge source's French translation") } } assert.True(t, foundEn, "English translation should be present") assert.True(t, foundFr, "French translation should be present") assert.Len(t, finalTargetWork.Authors, 2, "Authors should be merged") assert.Len(t, finalTargetWork.Tags, 2, "Tags should be merged") // 3. Stats should be merged var finalStats domain.WorkStats db.Where("work_id = ?", targetWork.ID).First(&finalStats) assert.Equal(t, int64(30), finalStats.Views, "Views should be summed") assert.Equal(t, int64(15), finalStats.Likes, "Likes should be summed") // 4. Source stats should be deleted var deletedStats domain.WorkStats err = db.First(&deletedStats, "work_id = ?", sourceWork.ID).Error assert.Error(t, err, "Source stats should be deleted") assert.True(t, errors.Is(err, gorm.ErrRecordNotFound)) }