tercul-backend/internal/app/analytics/service_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

274 lines
8.7 KiB
Go

package analytics_test
import (
"context"
"strings"
"testing"
"tercul/internal/app/analytics"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/jobs/linguistics"
"tercul/internal/platform/config"
"tercul/internal/testutil"
"github.com/stretchr/testify/suite"
)
// AnalyticsServiceTestSuite is a test suite for the analytics service.
// It embeds the IntegrationTestSuite to get access to the database, app, etc.
type AnalyticsServiceTestSuite struct {
testutil.IntegrationTestSuite
service analytics.Service
}
// SetupSuite sets up the test suite with a real database and a real analytics service.
func (s *AnalyticsServiceTestSuite) SetupSuite() {
// Call the parent suite's setup
s.IntegrationTestSuite.SetupSuite(nil)
// Create a real analytics service with the test database
cfg, err := config.LoadConfig()
s.Require().NoError(err)
analyticsRepo := sql.NewAnalyticsRepository(s.DB, cfg)
analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB)
translationRepo := sql.NewTranslationRepository(s.DB, cfg)
workRepo := sql.NewWorkRepository(s.DB, cfg)
sentimentProvider, err := linguistics.NewGoVADERSentimentProvider()
s.Require().NoError(err)
// Create the service to be tested
s.service = analytics.NewService(analyticsRepo, analysisRepo, translationRepo, workRepo, sentimentProvider)
}
// SetupTest cleans the database before each test.
func (s *AnalyticsServiceTestSuite) SetupTest() {
s.IntegrationTestSuite.SetupTest()
s.DB.Exec("DELETE FROM trendings")
}
func (s *AnalyticsServiceTestSuite) TestIncrementWorkViews() {
s.Run("should increment the view count for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
// Act
err := s.service.IncrementWorkViews(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(int64(1), stats.Views)
})
}
func (s *AnalyticsServiceTestSuite) TestIncrementWorkLikes() {
s.Run("should increment the like count for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
// Act
err := s.service.IncrementWorkLikes(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(int64(1), stats.Likes)
})
}
func (s *AnalyticsServiceTestSuite) TestIncrementWorkComments() {
s.Run("should increment the comment count for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
// Act
err := s.service.IncrementWorkComments(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(int64(1), stats.Comments)
})
}
func (s *AnalyticsServiceTestSuite) TestIncrementWorkBookmarks() {
s.Run("should increment the bookmark count for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
// Act
err := s.service.IncrementWorkBookmarks(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(int64(1), stats.Bookmarks)
})
}
func (s *AnalyticsServiceTestSuite) TestIncrementWorkShares() {
s.Run("should increment the share count for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
// Act
err := s.service.IncrementWorkShares(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(int64(1), stats.Shares)
})
}
func (s *AnalyticsServiceTestSuite) TestIncrementWorkTranslationCount() {
s.Run("should increment the translation count for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
// Act
err := s.service.IncrementWorkTranslationCount(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(int64(1), stats.TranslationCount)
})
}
func (s *AnalyticsServiceTestSuite) TestUpdateWorkReadingTime() {
s.Run("should update the reading time for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
s.DB.Create(&domain.ReadabilityScore{WorkID: work.ID})
s.DB.Create(&domain.LanguageAnalysis{WorkID: work.ID, Analysis: domain.JSONB{}})
textMetadata := &domain.TextMetadata{
WorkID: work.ID,
WordCount: 1000,
}
s.DB.Create(textMetadata)
// Act
err := s.service.UpdateWorkReadingTime(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(5, stats.ReadingTime) // 1000 words / 200 wpm = 5 minutes
})
}
func (s *AnalyticsServiceTestSuite) TestUpdateTranslationReadingTime() {
s.Run("should update the reading time for a translation", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
translation := s.CreateTestTranslation(work.ID, "es", strings.Repeat("Contenido de prueba con quinientas palabras. ", 100))
// Act
err := s.service.UpdateTranslationReadingTime(context.Background(), translation.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateTranslationStats(context.Background(), translation.ID)
s.Require().NoError(err)
s.Equal(3, stats.ReadingTime) // 500 words / 200 wpm = 2.5 -> 3 minutes
})
}
func (s *AnalyticsServiceTestSuite) TestUpdateWorkComplexity() {
s.Run("should update the complexity for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
s.DB.Create(&domain.TextMetadata{WorkID: work.ID})
s.DB.Create(&domain.LanguageAnalysis{WorkID: work.ID, Analysis: domain.JSONB{}})
readabilityScore := &domain.ReadabilityScore{
WorkID: work.ID,
Score: 12.34,
}
s.DB.Create(readabilityScore)
// Act
err := s.service.UpdateWorkComplexity(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(12.34, stats.Complexity)
})
}
func (s *AnalyticsServiceTestSuite) TestUpdateWorkSentiment() {
s.Run("should update the sentiment for a work", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
s.DB.Create(&domain.TextMetadata{WorkID: work.ID})
s.DB.Create(&domain.ReadabilityScore{WorkID: work.ID})
languageAnalysis := &domain.LanguageAnalysis{
WorkID: work.ID,
Analysis: domain.JSONB{
"sentiment": 0.5678,
},
}
s.DB.Create(languageAnalysis)
// Act
err := s.service.UpdateWorkSentiment(context.Background(), work.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateWorkStats(context.Background(), work.ID)
s.Require().NoError(err)
s.Equal(0.5678, stats.Sentiment)
})
}
func (s *AnalyticsServiceTestSuite) TestUpdateTranslationSentiment() {
s.Run("should update the sentiment for a translation", func() {
// Arrange
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
translation := s.CreateTestTranslation(work.ID, "en", "This is a wonderfully positive and uplifting sentence.")
// Act
err := s.service.UpdateTranslationSentiment(context.Background(), translation.ID)
s.Require().NoError(err)
// Assert
stats, err := s.service.GetOrCreateTranslationStats(context.Background(), translation.ID)
s.Require().NoError(err)
s.True(stats.Sentiment > 0.5)
})
}
func (s *AnalyticsServiceTestSuite) TestUpdateTrending() {
s.Run("should update the trending works", func() {
// Arrange
work1 := s.CreateTestWork(s.AdminCtx, "Work 1", "en", "content")
work2 := s.CreateTestWork(s.AdminCtx, "Work 2", "en", "content")
s.DB.Create(&domain.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
s.DB.Create(&domain.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
// Act
err := s.service.UpdateTrending(context.Background())
s.Require().NoError(err)
// Assert
var trendingWorks []*domain.Trending
s.DB.Order("rank asc").Find(&trendingWorks)
s.Require().Len(trendingWorks, 2)
s.Equal(work2.ID, trendingWorks[0].EntityID)
s.Equal(work1.ID, trendingWorks[1].EntityID)
})
}
// TestAnalyticsService runs the full test suite.
func TestAnalyticsService(t *testing.T) {
suite.Run(t, new(AnalyticsServiceTestSuite))
}