package commands import ( "context" "encoding/json" "os" "path/filepath" "testing" "time" "tercul/internal/domain" "tercul/internal/platform/log" "github.com/blevesearch/bleve/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) // mockTranslationRepository is a mock implementation of TranslationRepository for testing type mockTranslationRepository struct { translations []domain.Translation err error } func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) { if m.err != nil { return nil, m.err } return m.translations, nil } // Implement other required methods with minimal implementations func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { return nil, nil } func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error { return nil } func (m *mockTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error { return nil } func (m *mockTranslationRepository) Delete(ctx context.Context, id uint) error { return nil } func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil } func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { return nil, nil } func (m *mockTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil } func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { return nil, nil } func (m *mockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) { return nil, nil } func (m *mockTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) { return nil, nil } func (m *mockTranslationRepository) Upsert(ctx context.Context, translation *domain.Translation) error { return nil } func (m *mockTranslationRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil } func (m *mockTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return fn(nil) } func (m *mockTranslationRepository) Count(ctx context.Context) (int64, error) { return int64(len(m.translations)), nil } func (m *mockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return int64(len(m.translations)), nil } func (m *mockTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) { for _, t := range m.translations { if t.ID == id { return true, nil } } return false, nil } func (m *mockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) { for _, t := range m.translations { if t.ID == id { return &t, nil } } return nil, nil } func (m *mockTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) { return m.translations, nil } func (m *mockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) { start := offset end := offset + batchSize if end > len(m.translations) { end = len(m.translations) } if start >= len(m.translations) { return []domain.Translation{}, nil } return m.translations[start:end], nil } func (m *mockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil } func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil } func (m *mockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil } func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) { return m.GetByID(ctx, id) } // initBleveIndexForTest creates an in-memory Bleve index for faster testing func initBleveIndexForTest(t *testing.T) bleve.Index { mapping := bleve.NewIndexMapping() translationMapping := bleve.NewDocumentMapping() // Simplified mapping for tests idMapping := bleve.NewTextFieldMapping() idMapping.Store = true idMapping.Index = true idMapping.Analyzer = "keyword" translationMapping.AddFieldMappingsAt("id", idMapping) titleMapping := bleve.NewTextFieldMapping() titleMapping.Store = true titleMapping.Index = true titleMapping.Analyzer = "standard" translationMapping.AddFieldMappingsAt("title", titleMapping) contentMapping := bleve.NewTextFieldMapping() contentMapping.Store = true contentMapping.Index = true contentMapping.Analyzer = "standard" translationMapping.AddFieldMappingsAt("content", contentMapping) languageMapping := bleve.NewTextFieldMapping() languageMapping.Store = true languageMapping.Index = true languageMapping.Analyzer = "keyword" translationMapping.AddFieldMappingsAt("language", languageMapping) statusMapping := bleve.NewTextFieldMapping() statusMapping.Store = true statusMapping.Index = true statusMapping.Analyzer = "keyword" translationMapping.AddFieldMappingsAt("status", statusMapping) translatableIDMapping := bleve.NewNumericFieldMapping() translatableIDMapping.Store = true translatableIDMapping.Index = true translationMapping.AddFieldMappingsAt("translatable_id", translatableIDMapping) translatableTypeMapping := bleve.NewTextFieldMapping() translatableTypeMapping.Store = true translatableTypeMapping.Index = true translatableTypeMapping.Analyzer = "keyword" translationMapping.AddFieldMappingsAt("translatable_type", translatableTypeMapping) translatorIDMapping := bleve.NewNumericFieldMapping() translatorIDMapping.Store = true translatorIDMapping.Index = true translationMapping.AddFieldMappingsAt("translator_id", translatorIDMapping) mapping.AddDocumentMapping("translation", translationMapping) // Use in-memory index for tests index, err := bleve.NewMemOnly(mapping) require.NoError(t, err) return index } func TestInitBleveIndex(t *testing.T) { if testing.Short() { t.Skip("Skipping slow Bleve index test in short mode") } indexPath := filepath.Join(t.TempDir(), "test_index") // Create index first time index1, err := initBleveIndex(indexPath) require.NoError(t, err) require.NotNil(t, index1) // Close and reopen (don't use defer here since we're closing explicitly) err = index1.Close() require.NoError(t, err) // Try to open existing index index2, err := initBleveIndex(indexPath) assert.NoError(t, err) assert.NotNil(t, index2) if index2 != nil { defer index2.Close() } } func TestIndexBatch(t *testing.T) { index := initBleveIndexForTest(t) defer index.Close() translations := []domain.Translation{ { BaseModel: domain.BaseModel{ID: 1}, Title: "Test Translation 1", Content: "Content 1", Language: "en", Status: domain.TranslationStatusPublished, TranslatableID: 100, TranslatableType: "works", }, { BaseModel: domain.BaseModel{ID: 2}, Title: "Test Translation 2", Content: "Content 2", Language: "fr", Status: domain.TranslationStatusDraft, TranslatableID: 200, TranslatableType: "works", }, } // Use a test logger logger := getTestLogger() err := indexBatch(index, translations, logger) assert.NoError(t, err) // Verify documents are indexed doc1, err := index.Document("translation_1") assert.NoError(t, err) assert.NotNil(t, doc1) doc2, err := index.Document("translation_2") assert.NoError(t, err) assert.NotNil(t, doc2) } func TestCheckpointSaveAndLoad(t *testing.T) { // Use a temporary file for checkpoint testCheckpointFile := filepath.Join(t.TempDir(), "test_checkpoint.json") // Temporarily override the checkpoint file path by using a helper cp := &checkpoint{ LastProcessedID: 123, TotalProcessed: 456, LastUpdated: time.Now(), } // Save checkpoint to test file data, err := json.Marshal(cp) require.NoError(t, err) err = os.WriteFile(testCheckpointFile, data, 0644) require.NoError(t, err) // Load checkpoint from test file data, err = os.ReadFile(testCheckpointFile) require.NoError(t, err) var loaded checkpoint err = json.Unmarshal(data, &loaded) require.NoError(t, err) assert.Equal(t, cp.LastProcessedID, loaded.LastProcessedID) assert.Equal(t, cp.TotalProcessed, loaded.TotalProcessed) } func TestMigrateTranslations(t *testing.T) { index := initBleveIndexForTest(t) defer index.Close() translations := []domain.Translation{ { BaseModel: domain.BaseModel{ID: 1}, Title: "Test 1", Content: "Content 1", Language: "en", Status: domain.TranslationStatusPublished, TranslatableID: 100, TranslatableType: "works", }, { BaseModel: domain.BaseModel{ID: 2}, Title: "Test 2", Content: "Content 2", Language: "fr", Status: domain.TranslationStatusPublished, TranslatableID: 200, TranslatableType: "works", }, } repo := &mockTranslationRepository{translations: translations} logger := getTestLogger() stats, err := migrateTranslations( context.Background(), repo, index, 10, // small batch size for testing nil, // no checkpoint logger, context.Background(), ) assert.NoError(t, err) assert.NotNil(t, stats) assert.Equal(t, 2, stats.TotalIndexed) assert.Equal(t, 0, stats.TotalErrors) } func TestMigrateTranslationsWithCheckpoint(t *testing.T) { index := initBleveIndexForTest(t) defer index.Close() translations := []domain.Translation{ { BaseModel: domain.BaseModel{ID: 1}, Title: "Test 1", Content: "Content 1", Language: "en", Status: domain.TranslationStatusPublished, TranslatableID: 100, TranslatableType: "works", }, { BaseModel: domain.BaseModel{ID: 2}, Title: "Test 2", Content: "Content 2", Language: "fr", Status: domain.TranslationStatusPublished, TranslatableID: 200, TranslatableType: "works", }, { BaseModel: domain.BaseModel{ID: 3}, Title: "Test 3", Content: "Content 3", Language: "de", Status: domain.TranslationStatusPublished, TranslatableID: 300, TranslatableType: "works", }, } repo := &mockTranslationRepository{translations: translations} logger := getTestLogger() // Resume from checkpoint after ID 1 cp := &checkpoint{ LastProcessedID: 1, TotalProcessed: 1, LastUpdated: time.Now(), } stats, err := migrateTranslations( context.Background(), repo, index, 10, cp, logger, context.Background(), ) assert.NoError(t, err) assert.NotNil(t, stats) // Should only process translations with ID > 1 assert.Equal(t, 3, stats.TotalIndexed) // 1 from checkpoint + 2 new } func TestVerifyIndex(t *testing.T) { index := initBleveIndexForTest(t) defer index.Close() translations := []domain.Translation{ { BaseModel: domain.BaseModel{ID: 1}, Title: "Test 1", Content: "Content 1", Language: "en", Status: domain.TranslationStatusPublished, TranslatableID: 100, TranslatableType: "works", }, } repo := &mockTranslationRepository{translations: translations} logger := getTestLogger() // Index the translation first err := indexBatch(index, translations, logger) require.NoError(t, err) // Verify err = verifyIndex(index, repo, logger, context.Background()) assert.NoError(t, err) } func TestVerifyIndexWithMissingTranslation(t *testing.T) { index := initBleveIndexForTest(t) defer index.Close() translations := []domain.Translation{ { BaseModel: domain.BaseModel{ID: 1}, Title: "Test 1", Content: "Content 1", Language: "en", Status: domain.TranslationStatusPublished, TranslatableID: 100, TranslatableType: "works", }, } repo := &mockTranslationRepository{translations: translations} logger := getTestLogger() // Don't index - verification should fail err := verifyIndex(index, repo, logger, context.Background()) assert.Error(t, err) assert.Contains(t, err.Error(), "missing from index") } // getTestLogger creates a test logger instance func getTestLogger() *log.Logger { log.Init("test", "test") return log.FromContext(context.Background()) }