tercul-backend/internal/testutil/integration_test_utils.go
google-labs-jules[bot] a68db7b694 refactor(app): move composition root to main.go
Refactored the application's dependency injection and server setup to improve modularity and adhere to the Dependency Inversion Principle.

- Moved the instantiation of all application services from `internal/app/app.go` to the composition root in `cmd/api/main.go`.
- The `app.NewApplication` function now accepts pre-built service interfaces, making the `app` package a simple container.
- Updated `internal/testutil/integration_test_utils.go` to reflect the new DI pattern, ensuring tests align with the refactored structure.
- Corrected build errors that arose from the refactoring, including import conflicts and incorrect function calls.
- Updated `TASKS.md` to mark the 'Refactor Dependency Injection' task as complete.
2025-10-07 14:05:19 +00:00

263 lines
8.5 KiB
Go

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)
}