tercul-backend/internal/testutil/integration_test_utils.go
google-labs-jules[bot] 1655a02a08 Refactor repository tests to be more DRY and maintainable.
Introduced a new testing strategy for the data access layer to avoid redundant testing of generic repository methods.

- Created a comprehensive test suite for the generic `BaseRepository` using a dedicated `TestEntity`. This suite covers all common CRUD operations, including transactions and error handling, in a single location.
- Added a new, focused test suite for `CategoryRepository` that only tests its repository-specific methods, relying on the base repository tests for generic functionality.
- Refactored the existing `AuthorRepository` test suite to remove redundant CRUD tests, aligning it with the new, cleaner pattern.
- Updated the test utilities to support the new testing strategy.

This change significantly improves the maintainability and efficiency of the test suite and provides a clear, future-proof pattern for testing all repositories.
2025-09-06 13:01:04 +00:00

421 lines
13 KiB
Go

package testutil
import (
"context"
"log"
"os"
"path/filepath"
"time"
"github.com/stretchr/testify/suite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
graph "tercul/internal/adapters/graphql"
"tercul/internal/app/auth"
auth_platform "tercul/internal/platform/auth"
"tercul/internal/app"
"tercul/internal/app/copyright"
"tercul/internal/app/localization"
"tercul/internal/app/monetization"
"tercul/internal/app/search"
"tercul/internal/app/work"
"tercul/internal/data/sql"
"tercul/internal/domain"
)
// IntegrationTestSuite provides a comprehensive test environment with either in-memory SQLite or mock repositories
type IntegrationTestSuite struct {
suite.Suite
App *app.Application
DB *gorm.DB
WorkRepo domain.WorkRepository
UserRepo domain.UserRepository
AuthorRepo domain.AuthorRepository
TranslationRepo domain.TranslationRepository
CommentRepo domain.CommentRepository
LikeRepo domain.LikeRepository
BookmarkRepo domain.BookmarkRepository
CollectionRepo domain.CollectionRepository
TagRepo domain.TagRepository
CategoryRepo domain.CategoryRepository
BookRepo domain.BookRepository
MonetizationRepo domain.MonetizationRepository
PublisherRepo domain.PublisherRepository
SourceRepo domain.SourceRepository
CopyrightRepo domain.CopyrightRepository
// Services
WorkCommands *work.WorkCommands
WorkQueries *work.WorkQueries
Localization localization.Service
AuthCommands *auth.AuthCommands
AuthQueries *auth.AuthQueries
// Test data
TestWorks []*domain.Work
TestUsers []*domain.User
TestAuthors []*domain.Author
TestTranslations []*domain.Translation
}
// TestConfig holds configuration for the test environment
type TestConfig struct {
UseInMemoryDB bool // If true, use SQLite in-memory, otherwise use mock repositories
DBPath string // Path for SQLite file (only used if UseInMemoryDB is false)
LogLevel logger.LogLevel
}
// DefaultTestConfig returns a default test configuration
func DefaultTestConfig() *TestConfig {
return &TestConfig{
UseInMemoryDB: true,
DBPath: "",
LogLevel: logger.Silent,
}
}
// SetupSuite sets up the test suite with the specified configuration
func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) {
if config == nil {
config = DefaultTestConfig()
}
if config.UseInMemoryDB {
s.setupInMemoryDB(config)
} else {
s.setupMockRepositories()
}
s.setupServices()
s.setupTestData()
}
// setupInMemoryDB sets up an in-memory SQLite database for testing
func (s *IntegrationTestSuite) setupInMemoryDB(config *TestConfig) {
var dbPath string
if config.DBPath != "" {
// Ensure directory exists
dir := filepath.Dir(config.DBPath)
if err := os.MkdirAll(dir, 0755); err != nil {
s.T().Fatalf("Failed to create database directory: %v", err)
}
dbPath = config.DBPath
} else {
// Use in-memory database
dbPath = ":memory:"
}
// Custom logger for tests
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: config.LogLevel,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
)
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: newLogger,
})
if err != nil {
s.T().Fatalf("Failed to connect to test database: %v", err)
}
s.DB = db
// Run migrations
if err := db.AutoMigrate(
&domain.Work{},
&domain.User{},
&domain.Author{},
&domain.Translation{},
&domain.Comment{},
&domain.Like{},
&domain.Bookmark{},
&domain.Collection{},
&domain.Tag{},
&domain.Category{},
&domain.Country{},
&domain.City{},
&domain.Place{},
&domain.Address{},
&domain.Copyright{},
&domain.CopyrightClaim{},
&domain.Monetization{},
&domain.Book{},
&domain.Publisher{},
&domain.Source{},
&domain.WorkCopyright{},
&domain.AuthorCopyright{},
&domain.BookCopyright{},
&domain.PublisherCopyright{},
&domain.SourceCopyright{},
&domain.WorkMonetization{},
&domain.AuthorMonetization{},
&domain.BookMonetization{},
&domain.PublisherMonetization{},
&domain.SourceMonetization{},
// &domain.WorkAnalytics{}, // Commented out as it's not in models package
&domain.ReadabilityScore{},
&domain.WritingStyle{},
&domain.Emotion{},
&domain.TopicCluster{},
&domain.Mood{},
&domain.Concept{},
&domain.LinguisticLayer{},
&domain.WorkStats{},
&domain.TextMetadata{},
&domain.PoeticAnalysis{},
&domain.TranslationField{},
&TestEntity{}, // Add TestEntity for generic repository tests
); err != nil {
s.T().Fatalf("Failed to run migrations: %v", err)
}
// Create repository instances
s.WorkRepo = sql.NewWorkRepository(db)
s.UserRepo = sql.NewUserRepository(db)
s.AuthorRepo = sql.NewAuthorRepository(db)
s.TranslationRepo = sql.NewTranslationRepository(db)
s.CommentRepo = sql.NewCommentRepository(db)
s.LikeRepo = sql.NewLikeRepository(db)
s.BookmarkRepo = sql.NewBookmarkRepository(db)
s.CollectionRepo = sql.NewCollectionRepository(db)
s.TagRepo = sql.NewTagRepository(db)
s.CategoryRepo = sql.NewCategoryRepository(db)
s.BookRepo = sql.NewBookRepository(db)
s.MonetizationRepo = sql.NewMonetizationRepository(db)
s.PublisherRepo = sql.NewPublisherRepository(db)
s.SourceRepo = sql.NewSourceRepository(db)
s.CopyrightRepo = sql.NewCopyrightRepository(db)
}
// setupMockRepositories sets up mock repositories for testing
func (s *IntegrationTestSuite) setupMockRepositories() {
s.WorkRepo = NewUnifiedMockWorkRepository()
// Temporarily comment out problematic repositories until we fix the interface implementations
// s.UserRepo = NewMockUserRepository()
// s.AuthorRepo = NewMockAuthorRepository()
// s.TranslationRepo = NewMockTranslationRepository()
// s.CommentRepo = NewMockCommentRepository()
// s.LikeRepo = NewMockLikeRepository()
// s.BookmarkRepo = NewMockBookmarkRepository()
// s.CollectionRepo = NewMockCollectionRepository()
// s.TagRepo = NewMockTagRepository()
// s.CategoryRepo = NewMockCategoryRepository()
}
// setupServices sets up service instances
func (s *IntegrationTestSuite) setupServices() {
mockAnalyzer := &MockAnalyzer{}
s.WorkCommands = work.NewWorkCommands(s.WorkRepo, mockAnalyzer)
s.WorkQueries = work.NewWorkQueries(s.WorkRepo)
s.Localization = localization.NewService(s.TranslationRepo)
jwtManager := auth_platform.NewJWTManager()
s.AuthCommands = auth.NewAuthCommands(s.UserRepo, jwtManager)
s.AuthQueries = auth.NewAuthQueries(s.UserRepo, jwtManager)
copyrightCommands := copyright.NewCopyrightCommands(s.CopyrightRepo)
copyrightQueries := copyright.NewCopyrightQueries(s.CopyrightRepo, s.WorkRepo, s.AuthorRepo, s.BookRepo, s.PublisherRepo, s.SourceRepo)
monetizationCommands := monetization.NewMonetizationCommands(s.MonetizationRepo)
monetizationQueries := monetization.NewMonetizationQueries(s.MonetizationRepo, s.WorkRepo, s.AuthorRepo, s.BookRepo, s.PublisherRepo, s.SourceRepo)
s.App = &app.Application{
WorkCommands: s.WorkCommands,
WorkQueries: s.WorkQueries,
AuthCommands: s.AuthCommands,
AuthQueries: s.AuthQueries,
CopyrightCommands: copyrightCommands,
CopyrightQueries: copyrightQueries,
Localization: s.Localization,
Search: search.NewIndexService(s.Localization, s.TranslationRepo),
MonetizationCommands: monetizationCommands,
MonetizationQueries: monetizationQueries,
AuthorRepo: s.AuthorRepo,
UserRepo: s.UserRepo,
TagRepo: s.TagRepo,
CategoryRepo: s.CategoryRepo,
BookRepo: s.BookRepo,
PublisherRepo: s.PublisherRepo,
SourceRepo: s.SourceRepo,
TranslationRepo: s.TranslationRepo,
CopyrightRepo: s.CopyrightRepo,
MonetizationRepo: s.MonetizationRepo,
CommentRepo: s.CommentRepo,
LikeRepo: s.LikeRepo,
BookmarkRepo: s.BookmarkRepo,
CollectionRepo: s.CollectionRepo,
}
}
// setupTestData creates initial test data
func (s *IntegrationTestSuite) setupTestData() {
// Create test users
s.TestUsers = []*domain.User{
{Username: "testuser1", Email: "test1@example.com", FirstName: "Test", LastName: "User1"},
{Username: "testuser2", Email: "test2@example.com", FirstName: "Test", LastName: "User2"},
}
for _, user := range s.TestUsers {
if err := s.UserRepo.Create(context.Background(), user); err != nil {
s.T().Logf("Warning: Failed to create test user: %v", err)
}
}
// Create test authors
s.TestAuthors = []*domain.Author{
{Name: "Test Author 1", TranslatableModel: domain.TranslatableModel{Language: "en"}},
{Name: "Test Author 2", TranslatableModel: domain.TranslatableModel{Language: "fr"}},
}
for _, author := range s.TestAuthors {
if err := s.AuthorRepo.Create(context.Background(), author); err != nil {
s.T().Logf("Warning: Failed to create test author: %v", err)
}
}
// Create test works
s.TestWorks = []*domain.Work{
{Title: "Test Work 1", TranslatableModel: domain.TranslatableModel{Language: "en"}},
{Title: "Test Work 2", TranslatableModel: domain.TranslatableModel{Language: "en"}},
{Title: "Test Work 3", TranslatableModel: domain.TranslatableModel{Language: "fr"}},
}
for _, work := range s.TestWorks {
if err := s.WorkRepo.Create(context.Background(), work); err != nil {
s.T().Logf("Warning: Failed to create test work: %v", err)
}
}
// Create test translations
s.TestTranslations = []*domain.Translation{
{
Title: "Test Work 1",
Content: "Test content for work 1",
Language: "en",
TranslatableID: s.TestWorks[0].ID,
TranslatableType: "Work",
IsOriginalLanguage: true,
},
{
Title: "Test Work 2",
Content: "Test content for work 2",
Language: "en",
TranslatableID: s.TestWorks[1].ID,
TranslatableType: "Work",
IsOriginalLanguage: true,
},
{
Title: "Test Work 3",
Content: "Test content for work 3",
Language: "fr",
TranslatableID: s.TestWorks[2].ID,
TranslatableType: "Work",
IsOriginalLanguage: true,
},
}
for _, translation := range s.TestTranslations {
if err := s.TranslationRepo.Create(context.Background(), translation); err != nil {
s.T().Logf("Warning: Failed to create test translation: %v", err)
}
}
}
// TearDownSuite cleans up the test suite
func (s *IntegrationTestSuite) TearDownSuite() {
if s.DB != nil {
sqlDB, err := s.DB.DB()
if err == nil {
sqlDB.Close()
}
}
}
// SetupTest resets test data for each test
func (s *IntegrationTestSuite) SetupTest() {
if s.DB != nil {
// Reset database for each test
s.DB.Exec("DELETE FROM translations")
s.DB.Exec("DELETE FROM works")
s.DB.Exec("DELETE FROM authors")
s.DB.Exec("DELETE FROM users")
s.setupTestData()
} else {
// Reset mock repositories
if mockRepo, ok := s.WorkRepo.(*UnifiedMockWorkRepository); ok {
mockRepo.Reset()
}
// Add similar reset logic for other mock repositories
}
}
// GetResolver returns a properly configured GraphQL resolver for testing
func (s *IntegrationTestSuite) GetResolver() *graph.Resolver {
return &graph.Resolver{
App: s.App,
}
}
// CreateTestWork creates a test work with optional content
func (s *IntegrationTestSuite) CreateTestWork(title, language string, content string) *domain.Work {
work := &domain.Work{
Title: title,
TranslatableModel: domain.TranslatableModel{Language: language},
}
if err := s.WorkRepo.Create(context.Background(), work); err != nil {
s.T().Fatalf("Failed to create test work: %v", err)
}
if content != "" {
translation := &domain.Translation{
Title: title,
Content: content,
Language: language,
TranslatableID: work.ID,
TranslatableType: "Work",
IsOriginalLanguage: true,
}
if err := s.TranslationRepo.Create(context.Background(), translation); err != nil {
s.T().Logf("Warning: Failed to create test translation: %v", err)
}
}
return work
}
// CleanupTestData removes all test data
func (s *IntegrationTestSuite) CleanupTestData() {
if s.DB != nil {
s.DB.Exec("DELETE FROM translations")
s.DB.Exec("DELETE FROM works")
s.DB.Exec("DELETE FROM authors")
s.DB.Exec("DELETE FROM users")
}
}
// CreateAuthenticatedUser creates a user and returns the user and an auth token
func (s *IntegrationTestSuite) CreateAuthenticatedUser(username, email string, role domain.UserRole) (*domain.User, string) {
user := &domain.User{
Username: username,
Email: email,
Role: role,
Password: "password", // Not used for token generation, but good to have
}
err := s.UserRepo.Create(context.Background(), user)
s.Require().NoError(err)
jwtManager := auth_platform.NewJWTManager()
token, err := jwtManager.GenerateToken(user)
s.Require().NoError(err)
return user, token
}