tercul-backend/internal/domain/interfaces.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

298 lines
13 KiB
Go

package domain
import (
"context"
"gorm.io/gorm"
"time"
)
// PaginatedResult represents a paginated result set
type PaginatedResult[T any] struct {
Items []T `json:"items"`
TotalCount int64 `json:"totalCount"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPages int `json:"totalPages"`
HasNext bool `json:"hasNext"`
HasPrev bool `json:"hasPrev"`
}
// MonetizationRepository defines CRUD methods specific to Monetization.
type MonetizationRepository interface {
BaseRepository[Monetization]
AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error
RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error
AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error
RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error
AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error
RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error
AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error
RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error
AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error
RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error
}
// PublisherRepository defines CRUD methods specific to Publisher.
type PublisherRepository interface {
BaseRepository[Publisher]
ListByCountryID(ctx context.Context, countryID uint) ([]Publisher, error)
}
// SourceRepository defines CRUD methods specific to Source.
type SourceRepository interface {
BaseRepository[Source]
ListByWorkID(ctx context.Context, workID uint) ([]Source, error)
FindByURL(ctx context.Context, url string) (*Source, error)
}
// BookRepository defines CRUD methods specific to Book.
type BookRepository interface {
BaseRepository[Book]
ListByAuthorID(ctx context.Context, authorID uint) ([]Book, error)
ListByPublisherID(ctx context.Context, publisherID uint) ([]Book, error)
ListByWorkID(ctx context.Context, workID uint) ([]Book, error)
FindByISBN(ctx context.Context, isbn string) (*Book, error)
}
// BookmarkRepository defines CRUD methods specific to Bookmark.
type BookmarkRepository interface {
BaseRepository[Bookmark]
ListByUserID(ctx context.Context, userID uint) ([]Bookmark, error)
ListByWorkID(ctx context.Context, workID uint) ([]Bookmark, error)
}
// CategoryRepository defines CRUD methods specific to Category.
type CategoryRepository interface {
BaseRepository[Category]
FindByName(ctx context.Context, name string) (*Category, error)
ListByWorkID(ctx context.Context, workID uint) ([]Category, error)
ListByParentID(ctx context.Context, parentID *uint) ([]Category, error)
}
// CityRepository defines CRUD methods specific to City.
type CityRepository interface {
BaseRepository[City]
ListByCountryID(ctx context.Context, countryID uint) ([]City, error)
}
// CollectionRepository defines CRUD methods specific to Collection.
type CollectionRepository interface {
BaseRepository[Collection]
ListByUserID(ctx context.Context, userID uint) ([]Collection, error)
ListPublic(ctx context.Context) ([]Collection, error)
ListByWorkID(ctx context.Context, workID uint) ([]Collection, error)
AddWorkToCollection(ctx context.Context, collectionID uint, workID uint) error
RemoveWorkFromCollection(ctx context.Context, collectionID uint, workID uint) error
}
// CommentRepository defines CRUD methods specific to Comment.
type CommentRepository interface {
BaseRepository[Comment]
ListByUserID(ctx context.Context, userID uint) ([]Comment, error)
ListByWorkID(ctx context.Context, workID uint) ([]Comment, error)
ListByTranslationID(ctx context.Context, translationID uint) ([]Comment, error)
ListByParentID(ctx context.Context, parentID uint) ([]Comment, error)
}
// ContributionRepository defines CRUD methods specific to Contribution.
type ContributionRepository interface {
BaseRepository[Contribution]
ListByUserID(ctx context.Context, userID uint) ([]Contribution, error)
ListByReviewerID(ctx context.Context, reviewerID uint) ([]Contribution, error)
ListByWorkID(ctx context.Context, workID uint) ([]Contribution, error)
ListByTranslationID(ctx context.Context, translationID uint) ([]Contribution, error)
ListByStatus(ctx context.Context, status string) ([]Contribution, error)
}
// CopyrightClaimRepository defines CRUD methods specific to CopyrightClaim.
type CopyrightClaimRepository interface {
BaseRepository[CopyrightClaim]
ListByWorkID(ctx context.Context, workID uint) ([]CopyrightClaim, error)
ListByUserID(ctx context.Context, userID uint) ([]CopyrightClaim, error)
}
// CountryRepository defines CRUD methods specific to Country.
type CountryRepository interface {
BaseRepository[Country]
GetByCode(ctx context.Context, code string) (*Country, error)
ListByContinent(ctx context.Context, continent string) ([]Country, error)
}
// EdgeRepository defines CRUD methods specific to Edge.
type EdgeRepository interface {
BaseRepository[Edge]
ListBySource(ctx context.Context, sourceTable string, sourceID uint) ([]Edge, error)
}
// EditionRepository defines CRUD methods specific to Edition.
type EditionRepository interface {
BaseRepository[Edition]
ListByBookID(ctx context.Context, bookID uint) ([]Edition, error)
FindByISBN(ctx context.Context, isbn string) (*Edition, error)
}
// EmailVerificationRepository defines CRUD methods specific to EmailVerification.
type EmailVerificationRepository interface {
BaseRepository[EmailVerification]
GetByToken(ctx context.Context, token string) (*EmailVerification, error)
GetByUserID(ctx context.Context, userID uint) ([]EmailVerification, error)
DeleteExpired(ctx context.Context) error
MarkAsUsed(ctx context.Context, id uint) error
}
// LikeRepository defines CRUD methods specific to Like.
type LikeRepository interface {
BaseRepository[Like]
ListByUserID(ctx context.Context, userID uint) ([]Like, error)
ListByWorkID(ctx context.Context, workID uint) ([]Like, error)
ListByTranslationID(ctx context.Context, translationID uint) ([]Like, error)
ListByCommentID(ctx context.Context, commentID uint) ([]Like, error)
}
// PasswordResetRepository defines CRUD methods specific to PasswordReset.
type PasswordResetRepository interface {
BaseRepository[PasswordReset]
GetByToken(ctx context.Context, token string) (*PasswordReset, error)
GetByUserID(ctx context.Context, userID uint) ([]PasswordReset, error)
DeleteExpired(ctx context.Context) error
MarkAsUsed(ctx context.Context, id uint) error
}
// PlaceRepository defines CRUD methods specific to Place.
type PlaceRepository interface {
BaseRepository[Place]
ListByCountryID(ctx context.Context, countryID uint) ([]Place, error)
ListByCityID(ctx context.Context, cityID uint) ([]Place, error)
FindNearby(ctx context.Context, latitude, longitude float64, radiusKm float64) ([]Place, error)
}
// TagRepository defines CRUD methods specific to Tag.
type TagRepository interface {
BaseRepository[Tag]
FindByName(ctx context.Context, name string) (*Tag, error)
ListByWorkID(ctx context.Context, workID uint) ([]Tag, error)
}
// TranslationRepository defines CRUD methods specific to Translation.
type TranslationRepository interface {
BaseRepository[Translation]
ListByWorkID(ctx context.Context, workID uint) ([]Translation, error)
ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*PaginatedResult[Translation], error)
ListByEntity(ctx context.Context, entityType string, entityID uint) ([]Translation, error)
ListByTranslatorID(ctx context.Context, translatorID uint) ([]Translation, error)
ListByStatus(ctx context.Context, status TranslationStatus) ([]Translation, error)
Upsert(ctx context.Context, translation *Translation) error
}
// UserRepository defines CRUD methods specific to User.
type UserRepository interface {
BaseRepository[User]
FindByUsername(ctx context.Context, username string) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
ListByRole(ctx context.Context, role UserRole) ([]User, error)
}
// UserProfileRepository defines CRUD methods specific to UserProfile.
type UserProfileRepository interface {
BaseRepository[UserProfile]
GetByUserID(ctx context.Context, userID uint) (*UserProfile, error)
}
// UserSessionRepository defines CRUD methods specific to UserSession.
type UserSessionRepository interface {
BaseRepository[UserSession]
GetByToken(ctx context.Context, token string) (*UserSession, error)
GetByUserID(ctx context.Context, userID uint) ([]UserSession, error)
DeleteExpired(ctx context.Context) error
}
// QueryOptions provides options for repository queries
type QueryOptions struct {
Preloads []string
OrderBy string
Where map[string]interface{}
Limit int
Offset int
}
// BaseRepository defines common CRUD operations that all repositories should implement
type BaseRepository[T any] interface {
Create(ctx context.Context, entity *T) error
CreateInTx(ctx context.Context, tx *gorm.DB, entity *T) error
GetByID(ctx context.Context, id uint) (*T, error)
GetByIDWithOptions(ctx context.Context, id uint, options *QueryOptions) (*T, error)
Update(ctx context.Context, entity *T) error
UpdateInTx(ctx context.Context, tx *gorm.DB, entity *T) error
Delete(ctx context.Context, id uint) error
DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error
List(ctx context.Context, page, pageSize int) (*PaginatedResult[T], error)
ListWithOptions(ctx context.Context, options *QueryOptions) ([]T, error)
ListAll(ctx context.Context) ([]T, error)
Count(ctx context.Context) (int64, error)
CountWithOptions(ctx context.Context, options *QueryOptions) (int64, error)
FindWithPreload(ctx context.Context, preloads []string, id uint) (*T, error)
GetAllForSync(ctx context.Context, batchSize, offset int) ([]T, error)
Exists(ctx context.Context, id uint) (bool, error)
BeginTx(ctx context.Context) (*gorm.DB, error)
WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error
}
// AuthorRepository defines CRUD methods specific to Author.
type AuthorRepository interface {
BaseRepository[Author]
FindByName(ctx context.Context, name string) (*Author, error)
ListByWorkID(ctx context.Context, workID uint) ([]Author, error)
ListByBookID(ctx context.Context, bookID uint) ([]Author, error)
ListByCountryID(ctx context.Context, countryID uint) ([]Author, error)
GetWithTranslations(ctx context.Context, id uint) (*Author, error)
}
// CopyrightRepository defines CRUD methods specific to Copyright.
type CopyrightRepository interface {
BaseRepository[Copyright]
AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error
RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error
AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error
RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error
AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error
RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error
AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error
RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error
AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error
RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error
AddTranslation(ctx context.Context, translation *CopyrightTranslation) error
GetTranslations(ctx context.Context, copyrightID uint) ([]CopyrightTranslation, error)
GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*CopyrightTranslation, error)
}
// WorkRepository defines methods specific to Work.
type WorkRepository interface {
BaseRepository[Work]
FindByTitle(ctx context.Context, title string) ([]Work, error)
FindByAuthor(ctx context.Context, authorID uint) ([]Work, error)
FindByCategory(ctx context.Context, categoryID uint) ([]Work, error)
FindByLanguage(ctx context.Context, language string, page, pageSize int) (*PaginatedResult[Work], error)
GetWithTranslations(ctx context.Context, id uint) (*Work, error)
GetWithAssociations(ctx context.Context, id uint) (*Work, error)
GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*Work, error)
ListWithTranslations(ctx context.Context, page, pageSize int) (*PaginatedResult[Work], error)
IsAuthor(ctx context.Context, workID uint, authorID uint) (bool, error)
ListByCollectionID(ctx context.Context, collectionID uint) ([]Work, error)
}
// AuthRepository defines the interface for authentication data access.
type AuthRepository interface {
StoreToken(ctx context.Context, userID uint, token string, expiresAt time.Time) error
DeleteToken(ctx context.Context, token string) error
}
// LocalizationRepository defines the interface for localization data access.
type LocalizationRepository interface {
GetTranslation(ctx context.Context, key string, language string) (string, error)
GetTranslations(ctx context.Context, keys []string, language string) (map[string]string, error)
GetAuthorBiography(ctx context.Context, authorID uint, language string) (string, error)
GetWorkContent(ctx context.Context, workID uint, language string) (string, error)
}