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" "github.com/hibiken/asynq" 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/analytics" "tercul/internal/app/monetization" "tercul/internal/app/search" "tercul/internal/app/work" "tercul/internal/data/sql" "tercul/internal/domain" "tercul/internal/jobs/linguistics" ) // 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 AsynqClient *asynq.Client Config *TestConfig 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 AnalyticsRepo domain.AnalyticsRepository AnalysisRepo linguistics.AnalysisRepository // Services WorkCommands *work.WorkCommands WorkQueries *work.WorkQueries Localization localization.Service AuthCommands *auth.AuthCommands AuthQueries *auth.AuthQueries AnalyticsService analytics.Service // 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 RedisAddr string } // DefaultTestConfig returns a default test configuration func DefaultTestConfig() *TestConfig { redisAddr := os.Getenv("REDIS_ADDR") if redisAddr == "" { redisAddr = "localhost:6379" } return &TestConfig{ UseInMemoryDB: true, DBPath: "", LogLevel: logger.Silent, RedisAddr: redisAddr, } } // SetupSuite sets up the test suite with the specified configuration func (s *IntegrationTestSuite) SetupSuite(config *TestConfig) { if config == nil { config = DefaultTestConfig() } s.Config = config if config.UseInMemoryDB { s.setupInMemoryDB(config) } else { s.setupMockRepositories() } s.AsynqClient = asynq.NewClient(asynq.RedisClientOpt{ Addr: config.RedisAddr, }) 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.WorkStats{}, &domain.TranslationStats{}, // &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.TranslationStats{}, &domain.UserEngagement{}, &domain.Trending{}, &domain.TextMetadata{}, &domain.PoeticAnalysis{}, &domain.LanguageAnalysis{}, &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) s.AnalyticsRepo = sql.NewAnalyticsRepository(db) s.AnalysisRepo = linguistics.NewGORMAnalysisRepository(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) sentimentProvider, _ := linguistics.NewGoVADERSentimentProvider() s.AnalyticsService = analytics.NewService(s.AnalyticsRepo, s.AnalysisRepo, s.TranslationRepo, s.WorkRepo, sentimentProvider) 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) analyticsPublisher := analytics.NewEventPublisher(s.AsynqClient) s.App = &app.Application{ AnalyticsService: s.AnalyticsService, AnalyticsPublisher: analyticsPublisher, WorkCommands: s.WorkCommands, WorkQueries: s.WorkQueries, AuthCommands: s.AuthCommands, AuthQueries: s.AuthQueries, CopyrightCommands: copyrightCommands, CopyrightQueries: copyrightQueries, Localization: s.Localization, Search: search.NewIndexService(s.Localization, &MockWeaviateWrapper{}), 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.DB.Exec("DELETE FROM trendings") s.DB.Exec("DELETE FROM work_stats") s.DB.Exec("DELETE FROM translation_stats") } 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 } // CreateTestTranslation creates a test translation for a work func (s *IntegrationTestSuite) CreateTestTranslation(workID uint, language, content string) *domain.Translation { translation := &domain.Translation{ Title: "Test Translation", Content: content, Language: language, TranslatableID: workID, TranslatableType: "Work", } err := s.TranslationRepo.Create(context.Background(), translation) s.Require().NoError(err) return translation }