package sql_test import ( "context" "tercul/internal/app/analytics" "tercul/internal/data/sql" "tercul/internal/domain" "tercul/internal/platform/config" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // newTestAnalyticsRepoWithSQLite sets up an in-memory SQLite database for testing. func newTestAnalyticsRepoWithSQLite(t *testing.T) (analytics.Repository, *gorm.DB) { // Using "file::memory:?cache=shared" to ensure the in-memory database is shared across connections in the same process. db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) require.NoError(t, err) // Auto-migrate the necessary schemas err = db.AutoMigrate( &domain.Work{}, &domain.WorkStats{}, &domain.Translation{}, &domain.TranslationStats{}, &domain.Trending{}, &domain.UserEngagement{}, &domain.User{}, ) require.NoError(t, err) cfg := &config.Config{} repo := sql.NewAnalyticsRepository(db, cfg) // Clean up the database after the test t.Cleanup(func() { sqlDB, err := db.DB() require.NoError(t, err) err = sqlDB.Close() require.NoError(t, err) }) return repo, db } func TestAnalyticsRepository_IncrementWorkCounter(t *testing.T) { repo, db := newTestAnalyticsRepoWithSQLite(t) ctx := context.Background() // Setup: Create a work to associate stats with work := domain.Work{Title: "Test Work"} require.NoError(t, db.Create(&work).Error) t.Run("creates_new_stats_if_not_exist", func(t *testing.T) { err := repo.IncrementWorkCounter(ctx, work.ID, "views", 5) require.NoError(t, err) var stats domain.WorkStats err = db.Where("work_id = ?", work.ID).First(&stats).Error require.NoError(t, err) assert.Equal(t, int64(5), stats.Views) }) t.Run("increments_existing_stats", func(t *testing.T) { // Increment again err := repo.IncrementWorkCounter(ctx, work.ID, "views", 3) require.NoError(t, err) var stats domain.WorkStats err = db.Where("work_id = ?", work.ID).First(&stats).Error require.NoError(t, err) assert.Equal(t, int64(8), stats.Views) // 5 + 3 }) t.Run("invalid_field", func(t *testing.T) { err := repo.IncrementWorkCounter(ctx, work.ID, "invalid_field", 1) assert.Error(t, err) }) } func TestAnalyticsRepository_GetTrendingWorks(t *testing.T) { repo, db := newTestAnalyticsRepoWithSQLite(t) ctx := context.Background() // Setup: Create some works and trending data work1 := domain.Work{Title: "Trending Work 1"} work2 := domain.Work{Title: "Trending Work 2"} require.NoError(t, db.Create(&work1).Error) require.NoError(t, db.Create(&work2).Error) trendingData := []*domain.Trending{ {EntityID: work1.ID, EntityType: "Work", Rank: 1, TimePeriod: "daily"}, {EntityID: work2.ID, EntityType: "Work", Rank: 2, TimePeriod: "daily"}, } require.NoError(t, db.Create(&trendingData).Error) t.Run("success", func(t *testing.T) { works, err := repo.GetTrendingWorks(ctx, "daily", 5) require.NoError(t, err) require.Len(t, works, 2) assert.Equal(t, work1.ID, works[0].ID) assert.Equal(t, work2.ID, works[1].ID) }) } func TestAnalyticsRepository_UpdateTrendingWorks(t *testing.T) { repo, db := newTestAnalyticsRepoWithSQLite(t) ctx := context.Background() // Setup: Create an old trending record oldTrending := domain.Trending{EntityID: 99, EntityType: "Work", Rank: 1, TimePeriod: "daily"} require.NoError(t, db.Create(&oldTrending).Error) newTrendingData := []*domain.Trending{ {EntityID: 1, EntityType: "Work", Rank: 1, Score: 100, TimePeriod: "daily"}, {EntityID: 2, EntityType: "Work", Rank: 2, Score: 90, TimePeriod: "daily"}, } err := repo.UpdateTrendingWorks(ctx, "daily", newTrendingData) require.NoError(t, err) var trendingResult []domain.Trending db.Where("time_period = ?", "daily").Order("rank asc").Find(&trendingResult) require.Len(t, trendingResult, 2) assert.Equal(t, uint(1), trendingResult[0].EntityID) assert.Equal(t, uint(2), trendingResult[1].EntityID) } func TestAnalyticsRepository_GetOrCreate(t *testing.T) { repo, db := newTestAnalyticsRepoWithSQLite(t) ctx := context.Background() work := domain.Work{Title: "Test Work"} require.NoError(t, db.Create(&work).Error) translation := domain.Translation{Title: "Test Translation", TranslatableID: work.ID, TranslatableType: "Work"} require.NoError(t, db.Create(&translation).Error) user := domain.User{Username: "testuser", Email: "test@test.com"} require.NoError(t, db.Create(&user).Error) t.Run("GetOrCreateWorkStats", func(t *testing.T) { // Create stats, err := repo.GetOrCreateWorkStats(ctx, work.ID) require.NoError(t, err) assert.Equal(t, work.ID, stats.WorkID) // Get stats2, err := repo.GetOrCreateWorkStats(ctx, work.ID) require.NoError(t, err) assert.Equal(t, stats.ID, stats2.ID) }) t.Run("GetOrCreateTranslationStats", func(t *testing.T) { // Create stats, err := repo.GetOrCreateTranslationStats(ctx, translation.ID) require.NoError(t, err) assert.Equal(t, translation.ID, stats.TranslationID) // Get stats2, err := repo.GetOrCreateTranslationStats(ctx, translation.ID) require.NoError(t, err) assert.Equal(t, stats.ID, stats2.ID) }) t.Run("GetOrCreateUserEngagement", func(t *testing.T) { date := time.Now().Truncate(24 * time.Hour) // Create eng, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date) require.NoError(t, err) assert.Equal(t, user.ID, eng.UserID) // Get eng2, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date) require.NoError(t, err) assert.Equal(t, eng.ID, eng2.ID) }) } func TestAnalyticsRepository_UpdateUserEngagement(t *testing.T) { repo, db := newTestAnalyticsRepoWithSQLite(t) ctx := context.Background() user := domain.User{Username: "testuser", Email: "test@test.com"} require.NoError(t, db.Create(&user).Error) date := time.Now().Truncate(24 * time.Hour) engagement, err := repo.GetOrCreateUserEngagement(ctx, user.ID, date) require.NoError(t, err) engagement.LikesGiven = 15 engagement.WorksRead = 10 err = repo.UpdateUserEngagement(ctx, engagement) require.NoError(t, err) var updatedEngagement domain.UserEngagement db.First(&updatedEngagement, engagement.ID) assert.Equal(t, 15, updatedEngagement.LikesGiven) assert.Equal(t, 10, updatedEngagement.WorksRead) } func TestAnalyticsRepository_IncrementTranslationCounter(t *testing.T) { repo, db := newTestAnalyticsRepoWithSQLite(t) ctx := context.Background() // Setup: Create a work and translation to associate stats with work := domain.Work{Title: "Test Work"} require.NoError(t, db.Create(&work).Error) translation := domain.Translation{Title: "Test Translation", TranslatableID: work.ID, TranslatableType: "works"} require.NoError(t, db.Create(&translation).Error) t.Run("creates_new_stats_if_not_exist", func(t *testing.T) { err := repo.IncrementTranslationCounter(ctx, translation.ID, "views", 10) require.NoError(t, err) var stats domain.TranslationStats err = db.Where("translation_id = ?", translation.ID).First(&stats).Error require.NoError(t, err) assert.Equal(t, int64(10), stats.Views) }) t.Run("increments_existing_stats", func(t *testing.T) { // Increment again err := repo.IncrementTranslationCounter(ctx, translation.ID, "views", 5) require.NoError(t, err) var stats domain.TranslationStats err = db.Where("translation_id = ?", translation.ID).First(&stats).Error require.NoError(t, err) assert.Equal(t, int64(15), stats.Views) // 10 + 5 }) t.Run("invalid_field", func(t *testing.T) { err := repo.IncrementTranslationCounter(ctx, translation.ID, "invalid_field", 1) assert.Error(t, err) }) } func TestAnalyticsRepository_UpdateWorkStats(t *testing.T) { repo, db := newTestAnalyticsRepoWithSQLite(t) ctx := context.Background() // Setup work := domain.Work{Title: "Test Work"} require.NoError(t, db.Create(&work).Error) stats := domain.WorkStats{WorkID: work.ID, Views: 10} require.NoError(t, db.Create(&stats).Error) // Act update := domain.WorkStats{ReadingTime: 120, Complexity: 0.5} err := repo.UpdateWorkStats(ctx, work.ID, update) require.NoError(t, err) // Assert var updatedStats domain.WorkStats err = db.Where("work_id = ?", work.ID).First(&updatedStats).Error require.NoError(t, err) assert.Equal(t, int64(10), updatedStats.Views) // Should not be zeroed assert.Equal(t, 120, updatedStats.ReadingTime) assert.Equal(t, 0.5, updatedStats.Complexity) } func TestAnalyticsRepository_UpdateTranslationStats(t *testing.T) { repo, db := newTestAnalyticsRepoWithSQLite(t) ctx := context.Background() // Setup work := domain.Work{Title: "Test Work"} require.NoError(t, db.Create(&work).Error) translation := domain.Translation{Title: "Test Translation", TranslatableID: work.ID, TranslatableType: "works"} require.NoError(t, db.Create(&translation).Error) stats := domain.TranslationStats{TranslationID: translation.ID, Views: 20} require.NoError(t, db.Create(&stats).Error) // Act update := domain.TranslationStats{ReadingTime: 60, Sentiment: 0.8} err := repo.UpdateTranslationStats(ctx, translation.ID, update) require.NoError(t, err) // Assert var updatedStats domain.TranslationStats err = db.Where("translation_id = ?", translation.ID).First(&updatedStats).Error require.NoError(t, err) assert.Equal(t, int64(20), updatedStats.Views) // Should not be zeroed assert.Equal(t, 60, updatedStats.ReadingTime) assert.Equal(t, 0.8, updatedStats.Sentiment) }