mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 02:51:34 +00:00
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.
291 lines
9.3 KiB
Go
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)
|
|
} |