turash/bugulma/backend/internal/handler/i18n_data_handler_test.go
2025-12-15 10:06:41 +01:00

315 lines
11 KiB
Go

package handler
import (
"bugulma/backend/internal/domain"
"bugulma/backend/internal/service"
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
type inMemoryLocalizationService struct {
data map[string]string
}
func newInMemoryLocalizationService() *inMemoryLocalizationService {
return &inMemoryLocalizationService{data: map[string]string{}}
}
func (s *inMemoryLocalizationService) key(entityType, entityID, field, locale string) string {
return entityType + ":" + entityID + ":" + field + ":" + locale
}
func (s *inMemoryLocalizationService) GetLocalizedValue(entityType, entityID, field, locale string) (string, error) {
return s.data[s.key(entityType, entityID, field, locale)], nil
}
func (s *inMemoryLocalizationService) SetLocalizedValue(entityType, entityID, field, locale, value string) error {
s.data[s.key(entityType, entityID, field, locale)] = value
return nil
}
func (s *inMemoryLocalizationService) GetAllLocalizedValues(entityType, entityID string) (map[string]map[string]string, error) {
return map[string]map[string]string{}, nil
}
func (s *inMemoryLocalizationService) GetLocalizedEntity(entityType, entityID, locale string) (map[string]string, error) {
return map[string]string{}, nil
}
func (s *inMemoryLocalizationService) GetSupportedLocalesForEntity(entityType, entityID string) ([]string, error) {
return []string{"ru", "en", "tt"}, nil
}
func (s *inMemoryLocalizationService) DeleteLocalizedValue(entityType, entityID, field, locale string) error {
delete(s.data, s.key(entityType, entityID, field, locale))
return nil
}
func (s *inMemoryLocalizationService) BulkSetLocalizedValues(entityType, entityID string, values map[string]map[string]string) error {
for field, locales := range values {
for locale, value := range locales {
s.data[s.key(entityType, entityID, field, locale)] = value
}
}
return nil
}
func (s *inMemoryLocalizationService) GetAllLocales() ([]string, error) {
return []string{"ru", "en", "tt"}, nil
}
func (s *inMemoryLocalizationService) SearchLocalizations(query, locale string, limit int) ([]*domain.Localization, error) {
return []*domain.Localization{}, nil
}
func (s *inMemoryLocalizationService) ApplyLocalizationToEntity(entity domain.Localizable, locale string) error {
return nil
}
type inMemoryLocalizationRepo struct {
entityFields map[string]map[string]map[string]string // entityType -> entityID -> field -> ruValue
locValues map[string]string // entityType:entityID:field:locale -> value
}
func newInMemoryLocalizationRepo() *inMemoryLocalizationRepo {
return &inMemoryLocalizationRepo{
entityFields: map[string]map[string]map[string]string{},
locValues: map[string]string{},
}
}
func (r *inMemoryLocalizationRepo) locKey(entityType, entityID, field, locale string) string {
return entityType + ":" + entityID + ":" + field + ":" + locale
}
func (r *inMemoryLocalizationRepo) Create(ctx context.Context, loc *domain.Localization) error {
r.locValues[r.locKey(loc.EntityType, loc.EntityID, loc.Field, loc.Locale)] = loc.Value
return nil
}
func (r *inMemoryLocalizationRepo) GetByEntityAndField(ctx context.Context, entityType, entityID, field, locale string) (*domain.Localization, error) {
if v, ok := r.locValues[r.locKey(entityType, entityID, field, locale)]; ok {
return &domain.Localization{EntityType: entityType, EntityID: entityID, Field: field, Locale: locale, Value: v}, nil
}
return nil, nil
}
func (r *inMemoryLocalizationRepo) GetAllByEntity(ctx context.Context, entityType, entityID string) ([]*domain.Localization, error) {
return []*domain.Localization{}, nil
}
func (r *inMemoryLocalizationRepo) Update(ctx context.Context, loc *domain.Localization) error {
r.locValues[r.locKey(loc.EntityType, loc.EntityID, loc.Field, loc.Locale)] = loc.Value
return nil
}
func (r *inMemoryLocalizationRepo) Delete(ctx context.Context, id string) error { return nil }
func (r *inMemoryLocalizationRepo) GetByEntityTypeAndLocale(ctx context.Context, entityType, locale string) ([]*domain.Localization, error) {
return []*domain.Localization{}, nil
}
func (r *inMemoryLocalizationRepo) GetAllLocales(ctx context.Context) ([]string, error) {
return []string{"ru", "en", "tt"}, nil
}
func (r *inMemoryLocalizationRepo) GetSupportedLocalesForEntity(ctx context.Context, entityType, entityID string) ([]string, error) {
return []string{"ru", "en", "tt"}, nil
}
func (r *inMemoryLocalizationRepo) BulkCreate(ctx context.Context, localizations []*domain.Localization) error {
for _, loc := range localizations {
_ = r.Create(ctx, loc)
}
return nil
}
func (r *inMemoryLocalizationRepo) BulkDelete(ctx context.Context, ids []string) error { return nil }
func (r *inMemoryLocalizationRepo) SearchLocalizations(ctx context.Context, query string, locale string, limit int) ([]*domain.Localization, error) {
return []*domain.Localization{}, nil
}
func (r *inMemoryLocalizationRepo) GetTranslationReuseCandidates(ctx context.Context, entityType, field, locale string) ([]domain.ReuseCandidate, error) {
return []domain.ReuseCandidate{}, nil
}
func (r *inMemoryLocalizationRepo) GetEntitiesNeedingTranslation(ctx context.Context, entityType, field, targetLocale string, limit int) ([]string, error) {
ids := []string{}
entities := r.entityFields[entityType]
for entityID, fields := range entities {
ruValue := fields[field]
if ruValue == "" {
continue
}
if r.locValues[r.locKey(entityType, entityID, field, targetLocale)] == "" {
ids = append(ids, entityID)
if limit > 0 && len(ids) >= limit {
break
}
}
}
return ids, nil
}
func (r *inMemoryLocalizationRepo) FindExistingTranslationByRussianText(ctx context.Context, entityType, field, targetLocale, russianText string) (string, error) {
return "", nil
}
func (r *inMemoryLocalizationRepo) GetTranslationCountsByEntity(ctx context.Context) (map[string]map[string]int, error) {
return map[string]map[string]int{}, nil
}
func (r *inMemoryLocalizationRepo) GetEntityFieldValue(ctx context.Context, entityType, entityID, field string) (string, error) {
if r.entityFields[entityType] == nil || r.entityFields[entityType][entityID] == nil {
return "", nil
}
return r.entityFields[entityType][entityID][field], nil
}
func TestI18nData_GetMissingTranslations_InvalidLocale(t *testing.T) {
gin.SetMode(gin.TestMode)
repo := newInMemoryLocalizationRepo()
locSvc := newInMemoryLocalizationService()
cacheSvc := service.NewTranslationCacheService(repo, locSvc)
translationSvc := service.NewTranslationService("http://localhost:0", "")
i18nSvc := service.NewI18nService(locSvc, repo, translationSvc, cacheSvc)
h := NewI18nHandler(i18nSvc)
r := gin.New()
r.GET("/api/v1/admin/i18n/data/:entityType/missing", h.GetMissingTranslations)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/i18n/data/organization/missing?locale=xx", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestI18nData_GetMissingTranslations_Success(t *testing.T) {
gin.SetMode(gin.TestMode)
repo := newInMemoryLocalizationRepo()
repo.entityFields["organization"] = map[string]map[string]string{
"org-1": {"name": "Имя 1", "description": "Описание 1"},
"org-2": {"name": "Имя 2", "description": "Описание 2"},
}
// Pretend org-1 name already translated
repo.locValues[repo.locKey("organization", "org-1", "name", "en")] = "Name 1"
locSvc := newInMemoryLocalizationService()
cacheSvc := service.NewTranslationCacheService(repo, locSvc)
translationSvc := service.NewTranslationService("http://localhost:0", "")
i18nSvc := service.NewI18nService(locSvc, repo, translationSvc, cacheSvc)
h := NewI18nHandler(i18nSvc)
r := gin.New()
r.GET("/api/v1/admin/i18n/data/:entityType/missing", h.GetMissingTranslations)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/i18n/data/organization/missing?locale=en&fields=name,description&limit=100", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp struct {
Total int `json:"total"`
Counts map[string]int `json:"counts"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
// name missing only for org-2, description missing for both
if resp.Counts["name"] != 1 {
t.Fatalf("expected name missing=1, got %d", resp.Counts["name"])
}
if resp.Counts["description"] != 2 {
t.Fatalf("expected description missing=2, got %d", resp.Counts["description"])
}
if resp.Total != 3 {
t.Fatalf("expected total=3, got %d", resp.Total)
}
}
func TestI18nData_BulkTranslateData_Success(t *testing.T) {
gin.SetMode(gin.TestMode)
// Fake Ollama server
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/generate" {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(service.TranslationResponse{
Model: "test",
Response: "TRANSLATED",
Done: true,
})
}))
defer ollama.Close()
repo := newInMemoryLocalizationRepo()
repo.entityFields["organization"] = map[string]map[string]string{
"org-1": {"name": "Имя 1"},
}
locSvc := newInMemoryLocalizationService()
cacheSvc := service.NewTranslationCacheService(repo, locSvc)
translationSvc := service.NewTranslationService(ollama.URL, "")
i18nSvc := service.NewI18nService(locSvc, repo, translationSvc, cacheSvc)
h := NewI18nHandler(i18nSvc)
r := gin.New()
r.POST("/api/v1/admin/i18n/data/bulk-translate", h.BulkTranslateData)
payload := map[string]any{
"entityType": "organization",
"entityIDs": []string{"org-1"},
"targetLocale": "en",
"fields": []string{"name"},
}
b, _ := json.Marshal(payload)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/i18n/data/bulk-translate", bytes.NewReader(b))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp struct {
Translated int `json:"translated"`
Results map[string]map[string]string `json:"results"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if resp.Translated != 1 {
t.Fatalf("expected translated=1, got %d", resp.Translated)
}
if resp.Results["org-1"]["name"] != "TRANSLATED" {
t.Fatalf("expected result TRANSLATED, got %q", resp.Results["org-1"]["name"])
}
// Also persisted via LocalizationService
if v, _ := locSvc.GetLocalizedValue("organization", "org-1", "name", "en"); v != "TRANSLATED" {
t.Fatalf("expected persisted TRANSLATED, got %q", v)
}
}