package testutil import ( "context" "log" "os" "path/filepath" "tercul/internal/app" "tercul/internal/app/analytics" "tercul/internal/app/translation" "tercul/internal/data/sql" "tercul/internal/domain" "tercul/internal/domain/search" platform_auth "tercul/internal/platform/auth" "tercul/internal/domain/work" "tercul/internal/jobs/linguistics" "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 *work.Work, pipeline string) error { return nil } // mockAnalyticsService is a mock implementation of the AnalyticsService interface. type mockAnalyticsService struct{} func (m *mockAnalyticsService) IncrementWorkViews(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementWorkLikes(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementWorkTranslationCount(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*work.WorkStats, error) { return &work.WorkStats{}, nil } func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) { return &domain.TranslationStats{}, nil } func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error { return nil } func (m *mockAnalyticsService) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error { return nil } func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error { return nil } func (m *mockAnalyticsService) UpdateTrending(ctx context.Context) error { return nil } func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*work.Work, error) { return nil, 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(config *TestConfig) { if config == nil { config = DefaultTestConfig() } var dbPath string if !config.UseInMemoryDB && config.DBPath != "" { // Clean up previous test database file before starting _ = os.Remove(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( &work.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{}, &work.WorkStats{}, &domain.Trending{}, &domain.UserSession{}, &domain.Localization{}, &domain.LanguageAnalysis{}, &domain.TextMetadata{}, &domain.ReadabilityScore{}, &domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{}, ) repos := sql.NewRepositories(s.DB) 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() deps := app.Dependencies{ WorkRepo: repos.Work, UserRepo: repos.User, AuthorRepo: repos.Author, TranslationRepo: repos.Translation, CommentRepo: repos.Comment, LikeRepo: repos.Like, BookmarkRepo: repos.Bookmark, CollectionRepo: repos.Collection, TagRepo: repos.Tag, CategoryRepo: repos.Category, BookRepo: repos.Book, PublisherRepo: repos.Publisher, SourceRepo: repos.Source, CopyrightRepo: repos.Copyright, MonetizationRepo: repos.Monetization, AnalyticsRepo: repos.Analytics, AuthRepo: repos.Auth, LocalizationRepo: repos.Localization, SearchClient: searchClient, AnalyticsService: analyticsService, JWTManager: jwtManager, } s.App = app.NewApplication(deps) // 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) *work.Work { work := &work.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) }