tercul-backend/internal/app/authz/authz.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

252 lines
6.7 KiB
Go

package authz
import (
"context"
"tercul/internal/domain"
platform_auth "tercul/internal/platform/auth"
)
// Service provides authorization checks for the application.
type Service struct {
workRepo domain.WorkRepository
authorRepo domain.AuthorRepository
userRepo domain.UserRepository
translationRepo domain.TranslationRepository
}
// NewService creates a new authorization service.
func NewService(workRepo domain.WorkRepository, authorRepo domain.AuthorRepository, userRepo domain.UserRepository, translationRepo domain.TranslationRepository) *Service {
return &Service{
workRepo: workRepo,
authorRepo: authorRepo,
userRepo: userRepo,
translationRepo: translationRepo,
}
}
// CanEditWork checks if a user has permission to edit a work.
// For now, we'll implement a simple rule: only an admin or the work's author can edit it.
func (s *Service) CanEditWork(ctx context.Context, userID uint, work *domain.Work) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
// Admins can do anything.
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return false, err
}
author, err := s.authorRepo.FindByName(ctx, user.Username)
if err != nil {
// If the author profile doesn't exist for the user, they can't be the author.
return false, nil
}
// Check if the user is an author of the work.
isAuthor, err := s.workRepo.IsAuthor(ctx, work.ID, author.ID)
if err != nil {
return false, err
}
if isAuthor {
return true, nil
}
return false, domain.ErrForbidden
}
// CanDeleteWork checks if a user has permission to delete a work.
func (s *Service) CanDeleteWork(ctx context.Context, userID uint, work *domain.Work) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
// Admins can do anything.
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
return false, err
}
author, err := s.authorRepo.FindByName(ctx, user.Username)
if err != nil {
// If the author profile doesn't exist for the user, they can't be the author.
return false, nil
}
// Check if the user is an author of the work.
isAuthor, err := s.workRepo.IsAuthor(ctx, work.ID, author.ID)
if err != nil {
return false, err
}
if isAuthor {
return true, nil
}
return false, domain.ErrForbidden
}
// CanEditEntity checks if a user has permission to edit a specific translatable entity.
func (s *Service) CanEditEntity(ctx context.Context, userID uint, translatableType string, translatableID uint) (bool, error) {
switch translatableType {
case "works":
// For works, we can reuse the CanEditWork logic.
// First, we need to fetch the work.
work, err := s.workRepo.GetByID(ctx, translatableID)
if err != nil {
return false, err // Handles not found, etc.
}
return s.CanEditWork(ctx, userID, work)
default:
// For now, deny all other types by default.
// This can be expanded later.
return false, domain.ErrForbidden
}
}
// CanDeleteTranslation checks if a user can delete a translation.
func (s *Service) CanDeleteTranslation(ctx context.Context, userID uint, translationID uint) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
// Admins can do anything.
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
translation, err := s.translationRepo.GetByID(ctx, translationID)
if err != nil {
return false, err
}
if translation.TranslatorID != nil && *translation.TranslatorID == userID {
return true, nil
}
return false, domain.ErrForbidden
}
// CanUpdateUser checks if a user has permission to update another user's profile.
func (s *Service) CanCreateWork(ctx context.Context) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
return false, domain.ErrForbidden
}
func (s *Service) CanCreateTranslation(ctx context.Context) (bool, error) {
_, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
return true, nil
}
func (s *Service) CanEditTranslation(ctx context.Context, userID uint, translationID uint) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
// Admins can do anything.
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
// Check if the user is the translator of the translation.
translation, err := s.translationRepo.GetByID(ctx, translationID)
if err != nil {
return false, err
}
if translation.TranslatorID != nil && *translation.TranslatorID == userID {
return true, nil
}
return false, domain.ErrForbidden
}
func (s *Service) CanCreateBook(ctx context.Context) (bool, error) {
_, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
return true, nil
}
func (s *Service) CanUpdateBook(ctx context.Context) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
return false, domain.ErrForbidden
}
func (s *Service) CanDeleteBook(ctx context.Context) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
return false, domain.ErrForbidden
}
func (s *Service) CanUpdateUser(ctx context.Context, actorID, targetUserID uint) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
// Admins can do anything.
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
// Users can update their own profile.
if actorID == targetUserID {
return true, nil
}
return false, domain.ErrForbidden
}
// CanDeleteComment checks if a user has permission to delete a comment.
// For now, we'll implement a simple rule: only an admin or the comment's author can delete it.
func (s *Service) CanDeleteComment(ctx context.Context, userID uint, comment *domain.Comment) (bool, error) {
claims, ok := platform_auth.GetClaimsFromContext(ctx)
if !ok {
return false, domain.ErrUnauthorized
}
// Admins can do anything.
if claims.Role == string(domain.UserRoleAdmin) {
return true, nil
}
// Check if the user is the author of the comment.
if comment.UserID == userID {
return true, nil
}
return false, domain.ErrForbidden
}