tercul-backend/cmd/cli/commands/bleve_migrate_test.go
google-labs-jules[bot] f2e93ede10 fix: resolve remaining lint errors
- Fix unhandled errors in tests (errcheck)
- Define constants for repeated strings (goconst)
- Suppress high complexity warnings with nolint:gocyclo
- Fix integer overflow warnings (gosec)
- Add package comments
- Split long lines (lll)
- Rename Analyse -> Analyze (misspell)
- Fix naked returns and unused params
2025-11-30 22:02:24 +00:00

434 lines
12 KiB
Go

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 func() { _ = index2.Close() }()
}
}
func TestIndexBatch(t *testing.T) {
index := initBleveIndexForTest(t)
defer func() { _ = 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
err := indexBatch(index, translations)
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 func() { _ = 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,
)
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 func() { _ = 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,
)
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 func() { _ = 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)
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 func() { _ = 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())
}