tercul-backend/internal/data/sql/analytics_repository_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

291 lines
9.3 KiB
Go

package sql_test
import (
"context"
"testing"
"tercul/internal/app/analytics"
"tercul/internal/data/sql"
"tercul/internal/domain"
"tercul/internal/platform/config"
"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)
}