package testutil import ( "context" "log" "os" "path/filepath" "tercul/internal/app" "tercul/internal/app/analytics" app_auth "tercul/internal/app/auth" "tercul/internal/app/author" "tercul/internal/app/authz" "tercul/internal/app/book" "tercul/internal/app/bookmark" "tercul/internal/app/category" "tercul/internal/app/collection" "tercul/internal/app/comment" "tercul/internal/app/contribution" "tercul/internal/app/like" "tercul/internal/app/localization" app_search "tercul/internal/app/search" "tercul/internal/app/tag" "tercul/internal/app/translation" "tercul/internal/app/user" "tercul/internal/app/work" "tercul/internal/data/sql" "tercul/internal/domain" "tercul/internal/domain/search" "tercul/internal/jobs/linguistics" platform_auth "tercul/internal/platform/auth" platform_config "tercul/internal/platform/config" "time" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) // mockSearchClient is a mock implementation of the SearchClient interface. type mockSearchClient struct{} func (m *mockSearchClient) IndexWork(ctx context.Context, work *domain.Work, pipeline string) error { return nil } // 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 AdminCtx context.Context } // 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 { // Using a file-based DB is more stable for integration tests across multiple packages // than the in-memory one, which can behave unpredictably with `go test ./...` return &TestConfig{ UseInMemoryDB: false, DBPath: "test.db", LogLevel: logger.Silent, } } // SetupSuite sets up the test suite with the specified configuration func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) { if testConfig == nil { testConfig = DefaultTestConfig() } var dbPath string if !testConfig.UseInMemoryDB && testConfig.DBPath != "" { // Clean up previous test database file before starting _ = os.Remove(testConfig.DBPath) // Ensure directory exists dir := filepath.Dir(testConfig.DBPath) if err := os.MkdirAll(dir, 0755); err != nil { s.T().Fatalf("Failed to create database directory: %v", err) } dbPath = testConfig.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: testConfig.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 err = db.AutoMigrate( &domain.Work{}, &domain.User{}, &domain.Author{}, &domain.Translation{}, &domain.Comment{}, &domain.Like{}, &domain.Bookmark{}, &domain.Collection{}, &domain.Tag{}, &domain.Category{}, &domain.Book{}, &domain.Publisher{}, &domain.Source{}, &domain.Copyright{}, &domain.Monetization{}, &domain.WorkStats{}, &domain.Trending{}, &domain.UserSession{}, &domain.Localization{}, &domain.LanguageAnalysis{}, &domain.TextMetadata{}, &domain.ReadabilityScore{}, &domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{}, ) s.Require().NoError(err, "Failed to migrate database schema") cfg, err := platform_config.LoadConfig() if err != nil { s.T().Fatalf("Failed to load config: %v", err) } repos := sql.NewRepositories(s.DB, cfg) var searchClient search.SearchClient = &mockSearchClient{} analysisRepo := linguistics.NewGORMAnalysisRepository(s.DB) sentimentProvider, err := linguistics.NewGoVADERSentimentProvider() if err != nil { s.T().Fatalf("Failed to create sentiment provider: %v", err) } analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider) jwtManager := platform_auth.NewJWTManager(cfg) authzService := authz.NewService(repos.Work, repos.Translation) authorService := author.NewService(repos.Author) bookService := book.NewService(repos.Book, authzService) bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService) categoryService := category.NewService(repos.Category) collectionService := collection.NewService(repos.Collection) commentService := comment.NewService(repos.Comment, authzService, analyticsService) contributionCommands := contribution.NewCommands(repos.Contribution, authzService) contributionService := contribution.NewService(contributionCommands) likeService := like.NewService(repos.Like, analyticsService) tagService := tag.NewService(repos.Tag) translationService := translation.NewService(repos.Translation, authzService) userService := user.NewService(repos.User, authzService, repos.UserProfile) localizationService := localization.NewService(repos.Localization) authService := app_auth.NewService(repos.User, jwtManager) workService := work.NewService(repos.Work, searchClient, authzService) searchService := app_search.NewService(searchClient, localizationService) s.App = app.NewApplication( authorService, bookService, bookmarkService, categoryService, collectionService, commentService, contributionService, likeService, tagService, translationService, userService, localizationService, authService, authzService, workService, searchService, analyticsService, ) // Create a default admin user for tests adminUser := &domain.User{ Username: "admin", Email: "admin@test.com", Role: domain.UserRoleAdmin, Active: true, } _ = adminUser.SetPassword("password") err = s.DB.Create(adminUser).Error s.Require().NoError(err) s.AdminCtx = ContextWithClaims(context.Background(), &platform_auth.Claims{ UserID: adminUser.ID, Role: string(adminUser.Role), }) } // 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") } } // 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, }, } // Note: CreateWork command might not exist or need context. Assuming it does for now. // If CreateWork also requires auth, this context should be s.AdminCtx createdWork, err := s.App.Work.Commands.CreateWork(s.AdminCtx, work) s.Require().NoError(err) if content != "" { translationInput := translation.CreateOrUpdateTranslationInput{ Title: title, Content: content, Language: language, TranslatableID: createdWork.ID, TranslatableType: "works", IsOriginalLanguage: true, // Assuming the first one is original } _, err = s.App.Translation.Commands.CreateOrUpdateTranslation(s.AdminCtx, translationInput) s.Require().NoError(err) } return createdWork } // CreateTestTranslation creates a test translation for a work. func (s *IntegrationTestSuite) CreateTestTranslation(workID uint, language, content string) *domain.Translation { translationInput := translation.CreateOrUpdateTranslationInput{ Title: "Test Translation", Content: content, Language: language, TranslatableID: workID, TranslatableType: "works", } createdTranslation, err := s.App.Translation.Commands.CreateOrUpdateTranslation(s.AdminCtx, translationInput) s.Require().NoError(err) return createdTranslation } // ContextWithClaims creates a new context with the given claims. func ContextWithClaims(ctx context.Context, claims *platform_auth.Claims) context.Context { return context.WithValue(ctx, platform_auth.ClaimsContextKey, claims) }