mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 05:11:34 +00:00
* feat: add security middleware, graphql apq, and improved linting - Add RateLimit, RequestValidation, and CORS middleware. - Configure middleware chain in API server. - Implement Redis cache for GraphQL Automatic Persisted Queries. - Add .golangci.yml and fix linting issues (shadowing, timeouts). * feat: security, caching and linting config - Fix .golangci.yml config for govet shadow check - (Previous changes: Security middleware, GraphQL APQ, Linting fixes) * 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 --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
434 lines
12 KiB
Go
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())
|
|
}
|