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 analyticsSvc *mockAnalyticsService commands *WorkCommands } func (s *WorkCommandsSuite) SetupTest() { s.repo = &mockWorkRepository{} s.searchClient = &mockSearchClient{} s.authzSvc = authz.NewService(s.repo, nil) s.analyticsSvc = &mockAnalyticsService{} s.commands = NewWorkCommands(s.repo, s.searchClient, s.authzSvc, s.analyticsSvc) } 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() { work := &domain.Work{ TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}}, Translations: []*domain.Translation{ {BaseModel: domain.BaseModel{ID: 101}}, {BaseModel: domain.BaseModel{ID: 102}}, }, } s.repo.getWithTranslationsFunc = func(ctx context.Context, id uint) (*domain.Work, error) { return work, nil } var readingTime, complexity, sentiment, tReadingTime, tSentiment int s.analyticsSvc.updateWorkReadingTimeFunc = func(ctx context.Context, workID uint) error { readingTime++ return nil } s.analyticsSvc.updateWorkComplexityFunc = func(ctx context.Context, workID uint) error { complexity++ return nil } s.analyticsSvc.updateWorkSentimentFunc = func(ctx context.Context, workID uint) error { sentiment++ return nil } s.analyticsSvc.updateTranslationReadingTimeFunc = func(ctx context.Context, translationID uint) error { tReadingTime++ return nil } s.analyticsSvc.updateTranslationSentimentFunc = func(ctx context.Context, translationID uint) error { tSentiment++ return nil } err := s.commands.AnalyzeWork(context.Background(), 1) assert.NoError(s.T(), err) assert.Equal(s.T(), 1, readingTime, "UpdateWorkReadingTime should be called once") assert.Equal(s.T(), 1, complexity, "UpdateWorkComplexity should be called once") assert.Equal(s.T(), 1, sentiment, "UpdateWorkSentiment should be called once") assert.Equal(s.T(), 2, tReadingTime, "UpdateTranslationReadingTime should be called for each translation") assert.Equal(s.T(), 2, tSentiment, "UpdateTranslationSentiment should be called for each translation") } 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 analyticsSvc := &mockAnalyticsService{} commands := NewWorkCommands(workRepo, searchClient, authzSvc, analyticsSvc) // --- 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)) }