tercul-backend/internal/app/translation/commands.go
google-labs-jules[bot] c2e9a118e2 feat(testing): Increase test coverage and fix authz bugs
This commit significantly increases the test coverage across the application and fixes several underlying bugs that were discovered while writing the new tests.

The key changes include:

- **New Tests:** Added extensive integration and unit tests for GraphQL resolvers, application services, and data repositories, substantially increasing the test coverage for packages like `graphql`, `user`, `translation`, and `analytics`.

- **Authorization Bug Fixes:**
  - Fixed a critical bug where a user creating a `Work` was not correctly associated as its author, causing subsequent permission failures.
  - Corrected the authorization logic in `authz.Service` to properly check for entity ownership by non-admin users.

- **Test Refactoring:**
  - Refactored numerous test suites to use `testify/mock` instead of manual mocks, improving test clarity and maintainability.
  - Isolated integration tests by creating a fresh admin user and token for each test run, eliminating test pollution.
  - Centralized domain errors into `internal/domain/errors.go` and updated repositories to use them, making error handling more consistent.

- **Code Quality Improvements:**
  - Replaced manual mock implementations with `testify/mock` for better consistency.
  - Cleaned up redundant and outdated test files.

These changes stabilize the test suite, improve the overall quality of the codebase, and move the project closer to the goal of 80% test coverage.
2025-10-09 07:03:45 +00:00

119 lines
3.3 KiB
Go

package translation
import (
"context"
"fmt"
"tercul/internal/app/authz"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
// TranslationCommands contains the command handlers for the translation aggregate.
type TranslationCommands struct {
repo domain.TranslationRepository
authzSvc *authz.Service
tracer trace.Tracer
}
// NewTranslationCommands creates a new TranslationCommands handler.
func NewTranslationCommands(repo domain.TranslationRepository, authzSvc *authz.Service) *TranslationCommands {
return &TranslationCommands{
repo: repo,
authzSvc: authzSvc,
tracer: otel.Tracer("translation.commands"),
}
}
// CreateOrUpdateTranslationInput represents the input for creating or updating a translation.
type CreateOrUpdateTranslationInput struct {
Title string
Content string
Description string
Language string
Status domain.TranslationStatus
TranslatableID uint
TranslatableType string
TranslatorID *uint
IsOriginalLanguage bool
}
// CreateOrUpdateTranslation creates a new translation or updates an existing one.
func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, input CreateOrUpdateTranslationInput) (*domain.Translation, error) {
ctx, span := c.tracer.Start(ctx, "CreateOrUpdateTranslation")
defer span.End()
// Validate input first
if input.Language == "" {
return nil, fmt.Errorf("%w: language cannot be empty", domain.ErrValidation)
}
if input.TranslatableID == 0 {
return nil, fmt.Errorf("%w: translatable ID cannot be zero", domain.ErrValidation)
}
if input.TranslatableType == "" {
return nil, fmt.Errorf("%w: translatable type cannot be empty", domain.ErrValidation)
}
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return nil, domain.ErrUnauthorized
}
// Authorize that the user can edit the parent entity.
can, err := c.authzSvc.CanEditEntity(ctx, userID, input.TranslatableType, input.TranslatableID)
if err != nil {
return nil, fmt.Errorf("authorization check failed: %w", err)
}
if !can {
return nil, domain.ErrForbidden
}
var translatorID uint
if input.TranslatorID != nil {
translatorID = *input.TranslatorID
} else {
translatorID = userID
}
translation := &domain.Translation{
Title: input.Title,
Content: input.Content,
Description: input.Description,
Language: input.Language,
Status: input.Status,
TranslatableID: input.TranslatableID,
TranslatableType: input.TranslatableType,
TranslatorID: &translatorID,
IsOriginalLanguage: input.IsOriginalLanguage,
}
if err := c.repo.Upsert(ctx, translation); err != nil {
return nil, fmt.Errorf("failed to upsert translation: %w", err)
}
return translation, nil
}
// DeleteTranslation deletes a translation by ID.
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) error {
ctx, span := c.tracer.Start(ctx, "DeleteTranslation")
defer span.End()
userID, ok := platform_auth.GetUserIDFromContext(ctx)
if !ok {
return domain.ErrUnauthorized
}
can, err := c.authzSvc.CanDeleteTranslation(ctx, userID, id)
if err != nil {
return err
}
if !can {
return domain.ErrForbidden
}
return c.repo.Delete(ctx, id)
}