mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 04:01:34 +00:00
This commit introduces a comprehensive suite of unit tests for the application's models, repositories, and services, achieving 100% test coverage for all new and modified files. Key changes include: - Added unit tests for all services in `internal/app`. - Added unit tests for all repositories in `internal/data/sql`. - Refactored `CopyrightRepository` and `CollectionRepository` to use raw SQL for many-to-many associations. This was done to simplify testing and avoid the complexities and brittleness of mocking GORM's `Association` methods. - Removed a redundant and low-value test file for domain entities. - Fixed various build and test issues. - Addressed all feedback from the previous code review.
421 lines
13 KiB
Go
421 lines
13 KiB
Go
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"
|
|
|
|
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/monetization"
|
|
"tercul/internal/app/search"
|
|
"tercul/internal/app/work"
|
|
"tercul/internal/data/sql"
|
|
"tercul/internal/domain"
|
|
)
|
|
|
|
// 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
|
|
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
|
|
|
|
// Services
|
|
WorkCommands *work.WorkCommands
|
|
WorkQueries *work.WorkQueries
|
|
Localization localization.Service
|
|
AuthCommands *auth.AuthCommands
|
|
AuthQueries *auth.AuthQueries
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
|
|
if config.UseInMemoryDB {
|
|
s.setupInMemoryDB(config)
|
|
} else {
|
|
s.setupMockRepositories()
|
|
}
|
|
|
|
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.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.TextMetadata{},
|
|
&domain.PoeticAnalysis{},
|
|
&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)
|
|
}
|
|
|
|
// 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)
|
|
|
|
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)
|
|
|
|
s.App = &app.Application{
|
|
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.setupTestData()
|
|
} 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
|
|
}
|