package testutil import ( "context" "log" "os" "path/filepath" "tercul/internal/app" "tercul/internal/app/translation" "tercul/internal/data/sql" "tercul/internal/domain" "tercul/internal/domain/search" "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 Repos *sql.Repositories } // 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() } 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 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{}, ) s.Repos = sql.NewRepositories(s.DB) var searchClient search.SearchClient = &mockSearchClient{} s.App = app.NewApplication(s.Repos, searchClient, nil) } // 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, }, } createdWork, err := s.App.Work.Commands.CreateWork(context.Background(), work) s.Require().NoError(err) if content != "" { translationInput := translation.CreateTranslationInput{ Title: title, Content: content, Language: language, TranslatableID: createdWork.ID, TranslatableType: "works", } _, err = s.App.Translation.Commands.CreateTranslation(context.Background(), 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.CreateTranslationInput{ Title: "Test Translation", Content: content, Language: language, TranslatableID: workID, TranslatableType: "works", } createdTranslation, err := s.App.Translation.Commands.CreateTranslation(context.Background(), translationInput) s.Require().NoError(err) return createdTranslation }