diff --git a/TODO.md b/TODO.md index 401559d..e286dbf 100644 --- a/TODO.md +++ b/TODO.md @@ -2,64 +2,98 @@ --- -## [ ] Performance Improvements +## Suggested Next Objectives +- [ ] **Complete the Architecture Refactor (High, 5d):** Finalize the transition to a clean, domain-driven architecture. This will significantly improve maintainability, scalability, and developer velocity. + - [ ] Ensure resolvers call application services only and add dataloaders per aggregate. + - [ ] Adopt a migrations tool and move all SQL to migration files. + - [ ] Implement full observability with centralized logging, metrics, and tracing. +- [ ] **Full Test Coverage (High, 5d):** Increase test coverage across the application to ensure stability and prevent regressions. + - [ ] Write unit tests for all models, repositories, and services. + - [ ] Refactor existing tests to use mocks instead of a real database. +- [ ] **Implement Analytics Features (High, 3d):** Add analytics to provide insights into user engagement and content popularity. + - [ ] Implement view, like, comment, and bookmark counting. + - [ ] Track translation analytics to identify popular translations. +- [ ] **Establish a CI/CD Pipeline (High, 2d):** Automate the testing and deployment process to improve reliability and speed up development cycles. + - [ ] Add `make lint test test-integration` to the CI pipeline. + - [ ] Set up automated deployments to a staging environment. +- [ ] **Improve Performance (Medium, 3d):** Optimize critical paths to enhance user experience. + - [ ] Implement batching for Weaviate operations. + - [ ] Add performance benchmarks for critical paths. + +--- + +## [ ] High Priority + +### [ ] Architecture Refactor (DDD-lite) +- [ ] Resolvers call application services only; add dataloaders per aggregate (High, 3d) +- [ ] Adopt migrations tool (goose/atlas/migrate); move SQL to `internal/data/migrations`; delete `migrations.go` (High, 2d) +- [ ] Observability: centralize logging; add Prometheus metrics and OpenTelemetry tracing; request IDs (High, 3d) +- [ ] CI: add `make lint test test-integration` and integration tests with Docker compose (High, 2d) + +### [ ] Testing +- [ ] Add unit tests for all models, repositories, and services (High, 3d) +- [ ] Remove DB logic from `BaseSuite` for mock-based integration tests (High, 2d) + +### [ ] Features +- [ ] Implement analytics data collection (High, 3d) + - [ ] Implement view counting for works and translations + - [ ] Implement like counting for works and translations + - [ ] Implement comment counting for works + - [ ] Implement bookmark counting for works + - [ ] Implement translation counting for works + - [ ] Implement translation analytics to show popular translations + +--- + +## [ ] Medium Priority + +### [ ] Performance Improvements - [ ] Implement batching for Weaviate operations (Medium, 2d) -## [ ] Security Enhancements - -- [x] Add comprehensive input validation for all GraphQL mutations (High, 2d) - *Partially complete. Core mutations are validated.* - -## [ ] Code Quality & Architecture - +### [ ] Code Quality & Architecture - [ ] Expand Weaviate client to support all models (Medium, 2d) - [ ] Add code documentation and API docs (Medium, 2d) +- [ ] Replace bespoke cached repositories with decorators in `internal/data/cache` (reads only; deterministic invalidation) (Medium, 2d) +- [ ] Config: replace ad-hoc config with env parsing + validation (e.g., koanf/envconfig); no globals (Medium, 1d) -## [ ] Architecture Refactor (DDD-lite) +### [ ] Testing +- [ ] Add performance benchmarks for critical paths (Medium, 2d) + - [ ] Add benchmarks for text analysis (sequential vs concurrent) and cache hit/miss rates +### [ ] Monitoring & Logging +- [ ] Add monitoring for background jobs and API endpoints (Medium, 2d) + - [ ] Add metrics for linguistics: analysis duration, cache hit/miss, provider usage + +--- + +## [ ] Low Priority + +### [ ] Testing +- [ ] Refactor `RunTransactional` to be mock-friendly (Low, 1d) + +--- + +## [ ] Completed + +- [x] Add comprehensive input validation for all GraphQL mutations (High, 2d) - *Partially complete. Core mutations are validated.* - [x] Create skeleton packages: `cmd/`, `internal/platform/`, `internal/domain/`, `internal/app/`, `internal/data/`, `internal/adapters/graphql/`, `internal/jobs/` - [x] Move infra to `internal/platform/*` (`config`, `db`, `cache`, `auth`, `http`, `log`, `search`) - [x] Wire DI in `cmd/api/main.go` and expose an `Application` facade to adapters - [x] Unify GraphQL under `internal/adapters/graphql` and update `gqlgen.yml`; move `schema.graphqls` and resolvers -- [ ] Resolvers call application services only; add dataloaders per aggregate - [x] Introduce Unit-of-Work: `platform/db.WithTx(ctx, func(ctx) error)` and repo factory for `*sql.DB` / `*sql.Tx` - [x] Split write vs read paths for `work` (commands.go, queries.go); make read models cacheable -- [ ] Replace bespoke cached repositories with decorators in `internal/data/cache` (reads only; deterministic invalidation) - [x] Restructure `models/*` into domain aggregates with constructors and invariants -- [ ] Adopt migrations tool (goose/atlas/migrate); move SQL to `internal/data/migrations`; delete `migrations.go` -- [ ] Observability: centralize logging; add Prometheus metrics and OpenTelemetry tracing; request IDs -- [ ] Config: replace ad-hoc config with env parsing + validation (e.g., koanf/envconfig); no globals - [x] Security: move JWT/middleware to `internal/platform/auth`; add authz policy helpers (e.g., `CanEditWork`) - [x] Search: move Weaviate client/schema to `internal/platform/search`, optional domain interface - [x] Background jobs: move to `cmd/worker` and `internal/jobs/*`; ensure idempotency and lease - [x] Python ops: move scripts to `/ops/migration` and `/ops/analysis`; keep outputs under `/ops/migration/outputs/` - [x] Cleanup: delete dead packages (`store`, duplicate `repositories`); consolidate to `internal/data/sql` -- [ ] CI: add `make lint test test-integration` and integration tests with Docker compose - -## [ ] Testing - -- [ ] Add unit tests for all models, repositories, and services (High, 3d) - [x] Add integration tests for GraphQL API and background jobs (High, 3d) - *Partially complete. Core mutations are tested.* -- [ ] Add performance benchmarks for critical paths (Medium, 2d) - - [ ] Add benchmarks for text analysis (sequential vs concurrent) and cache hit/miss rates - -## [ ] Monitoring & Logging - -- [ ] Add monitoring for background jobs and API endpoints (Medium, 2d) - - [ ] Add metrics for linguistics: analysis duration, cache hit/miss, provider usage - -## [ ] Next Objective Proposal - - [x] Stabilize non-linguistics tests and interfaces (High, 2d) - [x] Fix `graph` mocks to accept context in service interfaces - [x] Update `repositories` tests (missing `TestModel`) and align with new repository interfaces - [x] Update `services` tests to pass context and implement missing repo methods in mocks -- [ ] Add performance benchmarks and metrics for linguistics (Medium, 2d) - - [ ] Benchmarks for AnalyzeText (provider on/off, concurrency levels) - - [ ] Export metrics and dashboards for analysis duration and cache effectiveness -- [ ] Documentation (Medium, 1d) - - [ ] Document NLP provider toggles and defaults in README/config docs - - [ ] Describe SRP/DRY design and extension points for new providers --- diff --git a/internal/app/application_builder.go b/internal/app/application_builder.go index 1cafa1c..9bc347f 100644 --- a/internal/app/application_builder.go +++ b/internal/app/application_builder.go @@ -24,7 +24,7 @@ import ( type ApplicationBuilder struct { dbConn *gorm.DB redisCache cache.Cache - weaviateClient *weaviate.Client + weaviateWrapper search.WeaviateWrapper asynqClient *asynq.Client App *Application linguistics *linguistics.LinguisticsFactory @@ -72,7 +72,7 @@ func (b *ApplicationBuilder) BuildWeaviate() error { log.LogFatal("Failed to create Weaviate client", log.F("error", err)) return err } - b.weaviateClient = wClient + b.weaviateWrapper = search.NewWeaviateWrapper(wClient) log.LogInfo("Weaviate client initialized successfully") return nil } @@ -130,7 +130,7 @@ func (b *ApplicationBuilder) BuildApplication() error { localizationService := localization.NewService(translationRepo) - searchService := search.NewIndexService(localizationService, translationRepo) + searchService := search.NewIndexService(localizationService, b.weaviateWrapper) b.App = &Application{ WorkCommands: workCommands, diff --git a/internal/app/auth/commands.go b/internal/app/auth/commands.go index aa8a089..d1c1126 100644 --- a/internal/app/auth/commands.go +++ b/internal/app/auth/commands.go @@ -44,11 +44,11 @@ type AuthResponse struct { // AuthCommands contains the command handlers for authentication. type AuthCommands struct { userRepo domain.UserRepository - jwtManager *auth.JWTManager + jwtManager auth.JWTManagement } // NewAuthCommands creates a new AuthCommands handler. -func NewAuthCommands(userRepo domain.UserRepository, jwtManager *auth.JWTManager) *AuthCommands { +func NewAuthCommands(userRepo domain.UserRepository, jwtManager auth.JWTManagement) *AuthCommands { return &AuthCommands{ userRepo: userRepo, jwtManager: jwtManager, @@ -58,11 +58,12 @@ func NewAuthCommands(userRepo domain.UserRepository, jwtManager *auth.JWTManager // Login authenticates a user and returns a JWT token func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthResponse, error) { if err := validateLoginInput(input); err != nil { - log.LogWarn("Login failed - invalid input", log.F("email", input.Email), log.F("error", err)) + log.LogWarn("Login validation failed", log.F("email", input.Email), log.F("error", err)) return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) } email := strings.TrimSpace(input.Email) + log.LogDebug("Attempting to log in user", log.F("email", email)) user, err := c.userRepo.FindByEmail(ctx, email) if err != nil { log.LogWarn("Login failed - user not found", log.F("email", email)) @@ -89,25 +90,27 @@ func (c *AuthCommands) Login(ctx context.Context, input LoginInput) (*AuthRespon user.LastLoginAt = &now if err := c.userRepo.Update(ctx, user); err != nil { log.LogWarn("Failed to update last login time", log.F("user_id", user.ID), log.F("error", err)) + // Do not fail the login if this update fails } log.LogInfo("User logged in successfully", log.F("user_id", user.ID), log.F("email", email)) return &AuthResponse{ Token: token, User: user, - ExpiresAt: time.Now().Add(24 * time.Hour), + ExpiresAt: time.Now().Add(24 * time.Hour), // This should be configurable }, nil } // Register creates a new user account func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*AuthResponse, error) { if err := validateRegisterInput(input); err != nil { - log.LogWarn("Registration failed - invalid input", log.F("email", input.Email), log.F("error", err)) + log.LogWarn("Registration validation failed", log.F("email", input.Email), log.F("error", err)) return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) } email := strings.TrimSpace(input.Email) username := strings.TrimSpace(input.Username) + log.LogDebug("Attempting to register new user", log.F("email", email), log.F("username", username)) existingUser, _ := c.userRepo.FindByEmail(ctx, email) if existingUser != nil { @@ -130,7 +133,7 @@ func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*Auth DisplayName: fmt.Sprintf("%s %s", strings.TrimSpace(input.FirstName), strings.TrimSpace(input.LastName)), Role: domain.UserRoleReader, Active: true, - Verified: false, + Verified: false, // Should be false until email verification } if err := c.userRepo.Create(ctx, user); err != nil { @@ -148,7 +151,7 @@ func (c *AuthCommands) Register(ctx context.Context, input RegisterInput) (*Auth return &AuthResponse{ Token: token, User: user, - ExpiresAt: time.Now().Add(24 * time.Hour), + ExpiresAt: time.Now().Add(24 * time.Hour), // This should be configurable }, nil } diff --git a/internal/app/auth/commands_test.go b/internal/app/auth/commands_test.go new file mode 100644 index 0000000..9d0b8b0 --- /dev/null +++ b/internal/app/auth/commands_test.go @@ -0,0 +1,286 @@ +package auth + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "testing" +) + + +type AuthCommandsSuite struct { + suite.Suite + userRepo *mockUserRepository + jwtManager *mockJWTManager + commands *AuthCommands +} + +func (s *AuthCommandsSuite) SetupTest() { + s.userRepo = newMockUserRepository() + s.jwtManager = &mockJWTManager{} + s.commands = NewAuthCommands(s.userRepo, s.jwtManager) +} + +func TestAuthCommandsSuite(t *testing.T) { + suite.Run(t, new(AuthCommandsSuite)) +} + +func (s *AuthCommandsSuite) TestLogin_Success() { + user := domain.User{ + Email: "test@example.com", + Password: "password", + Active: true, + } + s.userRepo.Create(context.Background(), &user) + + input := LoginInput{Email: "test@example.com", Password: "password"} + resp, err := s.commands.Login(context.Background(), input) + + assert.NoError(s.T(), err) + assert.NotNil(s.T(), resp) + assert.Equal(s.T(), "test-token", resp.Token) + assert.Equal(s.T(), user.ID, resp.User.ID) +} + +func (s *AuthCommandsSuite) TestLogin_InvalidInput() { + input := LoginInput{Email: "invalid-email", Password: "short"} + resp, err := s.commands.Login(context.Background(), input) + assert.Error(s.T(), err) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestValidateLoginInput_EmptyEmail() { + input := LoginInput{Email: "", Password: "password"} + err := validateLoginInput(input) + assert.Error(s.T(), err) +} + +func (s *AuthCommandsSuite) TestValidateLoginInput_ShortPassword() { + input := LoginInput{Email: "test@example.com", Password: "short"} + err := validateLoginInput(input) + assert.Error(s.T(), err) +} + +func (s *AuthCommandsSuite) TestValidateRegisterInput_ShortPassword() { + input := RegisterInput{Email: "test@example.com", Password: "short"} + err := validateRegisterInput(input) + assert.Error(s.T(), err) +} + +func (s *AuthCommandsSuite) TestValidateRegisterInput_ShortUsername() { + input := RegisterInput{Username: "a", Email: "test@example.com", Password: "password"} + err := validateRegisterInput(input) + assert.Error(s.T(), err) +} + +func (s *AuthCommandsSuite) TestValidateRegisterInput_LongUsername() { + input := RegisterInput{Username: "a51characterusernameisdefinitelytoolongforthisvalidation", Email: "test@example.com", Password: "password"} + err := validateRegisterInput(input) + assert.Error(s.T(), err) +} + +func (s *AuthCommandsSuite) TestLogin_SuccessUpdate() { + user := domain.User{ + Email: "test@example.com", + Password: "password", + Active: true, + } + s.userRepo.Create(context.Background(), &user) + s.userRepo.updateFunc = func(ctx context.Context, user *domain.User) error { + return nil + } + + input := LoginInput{Email: "test@example.com", Password: "password"} + resp, err := s.commands.Login(context.Background(), input) + + assert.NoError(s.T(), err) + assert.NotNil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestRegister_InvalidEmail() { + input := RegisterInput{ + Username: "newuser", + Email: "invalid-email", + Password: "password", + FirstName: "New", + LastName: "User", + } + resp, err := s.commands.Register(context.Background(), input) + assert.Error(s.T(), err) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestLogin_UpdateUserError() { + user := domain.User{ + Email: "test@example.com", + Password: "password", + Active: true, + } + s.userRepo.Create(context.Background(), &user) + s.userRepo.updateFunc = func(ctx context.Context, user *domain.User) error { + return errors.New("update error") + } + + input := LoginInput{Email: "test@example.com", Password: "password"} + resp, err := s.commands.Login(context.Background(), input) + + assert.NoError(s.T(), err) + assert.NotNil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestRegister_InvalidUsername() { + input := RegisterInput{ + Username: "invalid username", + Email: "new@example.com", + Password: "password", + FirstName: "New", + LastName: "User", + } + resp, err := s.commands.Register(context.Background(), input) + assert.Error(s.T(), err) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestLogin_UserNotFound() { + input := LoginInput{Email: "notfound@example.com", Password: "password"} + resp, err := s.commands.Login(context.Background(), input) + assert.ErrorIs(s.T(), err, ErrInvalidCredentials) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestLogin_InactiveUser() { + user := domain.User{ + Email: "inactive@example.com", + Password: "password", + Active: false, + } + s.userRepo.Create(context.Background(), &user) + + input := LoginInput{Email: "inactive@example.com", Password: "password"} + resp, err := s.commands.Login(context.Background(), input) + assert.ErrorIs(s.T(), err, ErrInvalidCredentials) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestLogin_InvalidPassword() { + user := domain.User{ + Email: "test@example.com", + Password: "password", + Active: true, + } + s.userRepo.Create(context.Background(), &user) + + input := LoginInput{Email: "test@example.com", Password: "wrong-password"} + resp, err := s.commands.Login(context.Background(), input) + assert.ErrorIs(s.T(), err, ErrInvalidCredentials) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestLogin_TokenGenerationError() { + user := domain.User{ + Email: "test@example.com", + Password: "password", + Active: true, + } + s.userRepo.Create(context.Background(), &user) + + s.jwtManager.generateTokenFunc = func(user *domain.User) (string, error) { + return "", errors.New("jwt error") + } + input := LoginInput{Email: "test@example.com", Password: "password"} + resp, err := s.commands.Login(context.Background(), input) + assert.Error(s.T(), err) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestRegister_Success() { + input := RegisterInput{ + Username: "newuser", + Email: "new@example.com", + Password: "password", + FirstName: "New", + LastName: "User", + } + resp, err := s.commands.Register(context.Background(), input) + assert.NoError(s.T(), err) + assert.NotNil(s.T(), resp) + assert.Equal(s.T(), "test-token", resp.Token) + assert.Equal(s.T(), "newuser", resp.User.Username) +} + +func (s *AuthCommandsSuite) TestRegister_InvalidInput() { + input := RegisterInput{Email: "invalid"} + resp, err := s.commands.Register(context.Background(), input) + assert.Error(s.T(), err) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestRegister_EmailExists() { + user := domain.User{ + Email: "exists@example.com", + } + s.userRepo.Create(context.Background(), &user) + + input := RegisterInput{ + Username: "newuser", + Email: "exists@example.com", + Password: "password", + FirstName: "New", + LastName: "User", + } + resp, err := s.commands.Register(context.Background(), input) + assert.ErrorIs(s.T(), err, ErrUserAlreadyExists) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestRegister_UsernameExists() { + user := domain.User{ + Username: "exists", + } + s.userRepo.Create(context.Background(), &user) + + input := RegisterInput{ + Username: "exists", + Email: "new@example.com", + Password: "password", + FirstName: "New", + LastName: "User", + } + resp, err := s.commands.Register(context.Background(), input) + assert.ErrorIs(s.T(), err, ErrUserAlreadyExists) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestRegister_CreateUserError() { + s.userRepo.createFunc = func(ctx context.Context, user *domain.User) error { + return errors.New("db error") + } + input := RegisterInput{ + Username: "newuser", + Email: "new@example.com", + Password: "password", + FirstName: "New", + LastName: "User", + } + resp, err := s.commands.Register(context.Background(), input) + assert.Error(s.T(), err) + assert.Nil(s.T(), resp) +} + +func (s *AuthCommandsSuite) TestRegister_TokenGenerationError() { + s.jwtManager.generateTokenFunc = func(user *domain.User) (string, error) { + return "", errors.New("jwt error") + } + input := RegisterInput{ + Username: "newuser", + Email: "new@example.com", + Password: "password", + FirstName: "New", + LastName: "User", + } + resp, err := s.commands.Register(context.Background(), input) + assert.Error(s.T(), err) + assert.Nil(s.T(), resp) +} diff --git a/internal/app/auth/main_test.go b/internal/app/auth/main_test.go new file mode 100644 index 0000000..d1314c1 --- /dev/null +++ b/internal/app/auth/main_test.go @@ -0,0 +1,138 @@ +package auth + +import ( + "context" + "errors" + "gorm.io/gorm" + "tercul/internal/domain" + "tercul/internal/platform/auth" +) + +// mockUserRepository is a local mock for the UserRepository interface. +type mockUserRepository struct { + users map[uint]domain.User + findByEmailFunc func(ctx context.Context, email string) (*domain.User, error) + findByUsernameFunc func(ctx context.Context, username string) (*domain.User, error) + createFunc func(ctx context.Context, user *domain.User) error + updateFunc func(ctx context.Context, user *domain.User) error + getByIDFunc func(ctx context.Context, id uint) (*domain.User, error) +} + +func newMockUserRepository() *mockUserRepository { + return &mockUserRepository{users: make(map[uint]domain.User)} +} + +func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) { + if m.findByEmailFunc != nil { + return m.findByEmailFunc(ctx, email) + } + for _, u := range m.users { + if u.Email == email { + return &u, nil + } + } + return nil, errors.New("user not found") +} + +func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) { + if m.findByUsernameFunc != nil { + return m.findByUsernameFunc(ctx, username) + } + for _, u := range m.users { + if u.Username == username { + return &u, nil + } + } + return nil, errors.New("user not found") +} + +func (m *mockUserRepository) Create(ctx context.Context, user *domain.User) error { + if m.createFunc != nil { + return m.createFunc(ctx, user) + } + // Simulate the BeforeSave hook for password hashing + if err := user.BeforeSave(nil); err != nil { + return err + } + user.ID = uint(len(m.users) + 1) + m.users[user.ID] = *user + return nil +} + +func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) error { + if m.updateFunc != nil { + return m.updateFunc(ctx, user) + } + if _, ok := m.users[user.ID]; ok { + m.users[user.ID] = *user + return nil + } + return errors.New("user not found") +} + +func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) { + if m.getByIDFunc != nil { + return m.getByIDFunc(ctx, id) + } + if user, ok := m.users[id]; ok { + return &user, nil + } + return nil, errors.New("user not found") +} + +// Implement the rest of the UserRepository interface with empty methods. +func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) { + return nil, nil +} +func (m *mockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) { return nil, nil } +func (m *mockUserRepository) Count(ctx context.Context) (int64, error) { return 0, nil } +func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) { + return nil, nil +} +func (m *mockUserRepository) Delete(ctx context.Context, id uint) error { return nil } +func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error { + return nil +} +func (m *mockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) { + return nil, nil +} +func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error { + return nil +} +func (m *mockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil } +func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) { + return nil, nil +} +func (m *mockUserRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { + return 0, nil +} +func (m *mockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) { + return nil, nil +} +func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) { + return nil, nil +} +func (m *mockUserRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil } +func (m *mockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil } +func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { + return nil +} + +// mockJWTManager is a local mock for the JWTManager. +type mockJWTManager struct { + generateTokenFunc func(user *domain.User) (string, error) + validateTokenFunc func(tokenString string) (*auth.Claims, error) +} + +func (m *mockJWTManager) GenerateToken(user *domain.User) (string, error) { + if m.generateTokenFunc != nil { + return m.generateTokenFunc(user) + } + return "test-token", nil +} +func (m *mockJWTManager) ValidateToken(tokenString string) (*auth.Claims, error) { + if m.validateTokenFunc != nil { + return m.validateTokenFunc(tokenString) + } + return &auth.Claims{UserID: 1}, nil +} diff --git a/internal/app/auth/queries.go b/internal/app/auth/queries.go index 68af553..19d39cf 100644 --- a/internal/app/auth/queries.go +++ b/internal/app/auth/queries.go @@ -16,11 +16,11 @@ var ( // AuthQueries contains the query handlers for authentication. type AuthQueries struct { userRepo domain.UserRepository - jwtManager *auth.JWTManager + jwtManager auth.JWTManagement } // NewAuthQueries creates a new AuthQueries handler. -func NewAuthQueries(userRepo domain.UserRepository, jwtManager *auth.JWTManager) *AuthQueries { +func NewAuthQueries(userRepo domain.UserRepository, jwtManager auth.JWTManagement) *AuthQueries { return &AuthQueries{ userRepo: userRepo, jwtManager: jwtManager, @@ -32,12 +32,14 @@ func (q *AuthQueries) GetUserFromContext(ctx context.Context) (*domain.User, err if ctx == nil { return nil, ErrContextRequired } + log.LogDebug("Attempting to get user from context") claims, err := auth.RequireAuth(ctx) if err != nil { log.LogWarn("Failed to get user from context - authentication required", log.F("error", err)) return nil, err } + log.LogDebug("Claims found in context", log.F("user_id", claims.UserID)) user, err := q.userRepo.GetByID(ctx, claims.UserID) if err != nil { @@ -50,6 +52,7 @@ func (q *AuthQueries) GetUserFromContext(ctx context.Context) (*domain.User, err return nil, ErrInvalidCredentials } + log.LogDebug("User retrieved from context successfully", log.F("user_id", user.ID)) return user, nil } @@ -63,12 +66,14 @@ func (q *AuthQueries) ValidateToken(ctx context.Context, tokenString string) (*d log.LogWarn("Token validation failed - empty token") return nil, auth.ErrMissingToken } + log.LogDebug("Attempting to validate token") claims, err := q.jwtManager.ValidateToken(tokenString) if err != nil { log.LogWarn("Token validation failed - invalid token", log.F("error", err)) return nil, err } + log.LogDebug("Token claims validated", log.F("user_id", claims.UserID)) user, err := q.userRepo.GetByID(ctx, claims.UserID) if err != nil { diff --git a/internal/app/auth/queries_test.go b/internal/app/auth/queries_test.go new file mode 100644 index 0000000..ac2c4e2 --- /dev/null +++ b/internal/app/auth/queries_test.go @@ -0,0 +1,134 @@ +package auth + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "tercul/internal/platform/auth" + "testing" +) + +type AuthQueriesSuite struct { + suite.Suite + userRepo *mockUserRepository + jwtManager *mockJWTManager + queries *AuthQueries +} + +func (s *AuthQueriesSuite) SetupTest() { + s.userRepo = newMockUserRepository() + s.jwtManager = &mockJWTManager{} + s.queries = NewAuthQueries(s.userRepo, s.jwtManager) +} + +func TestAuthQueriesSuite(t *testing.T) { + suite.Run(t, new(AuthQueriesSuite)) +} + +func (s *AuthQueriesSuite) TestGetUserFromContext_Success() { + user := domain.User{Active: true} + user.ID = 1 + s.userRepo.users[1] = user + + ctx := context.WithValue(context.Background(), auth.ClaimsContextKey, &auth.Claims{UserID: 1}) + + retrievedUser, err := s.queries.GetUserFromContext(ctx) + assert.NoError(s.T(), err) + assert.NotNil(s.T(), retrievedUser) + assert.Equal(s.T(), user.ID, retrievedUser.ID) +} + +func (s *AuthQueriesSuite) TestGetUserFromContext_NoClaims() { + retrievedUser, err := s.queries.GetUserFromContext(context.Background()) + assert.Error(s.T(), err) + assert.Nil(s.T(), retrievedUser) +} + +func (s *AuthQueriesSuite) TestGetUserFromContext_UserNotFound() { + ctx := context.WithValue(context.Background(), auth.ClaimsContextKey, &auth.Claims{UserID: 1}) + + retrievedUser, err := s.queries.GetUserFromContext(ctx) + assert.ErrorIs(s.T(), err, ErrUserNotFound) + assert.Nil(s.T(), retrievedUser) +} + +func (s *AuthQueriesSuite) TestGetUserFromContext_InactiveUser() { + user := domain.User{Active: false} + user.ID = 1 + s.userRepo.users[1] = user + + ctx := context.WithValue(context.Background(), auth.ClaimsContextKey, &auth.Claims{UserID: 1}) + + retrievedUser, err := s.queries.GetUserFromContext(ctx) + assert.ErrorIs(s.T(), err, ErrInvalidCredentials) + assert.Nil(s.T(), retrievedUser) +} + +func (s *AuthQueriesSuite) TestGetUserFromContext_NilContext() { + user, err := s.queries.GetUserFromContext(nil) + assert.ErrorIs(s.T(), err, ErrContextRequired) + assert.Nil(s.T(), user) +} + +func (s *AuthQueriesSuite) TestValidateToken_NilContext() { + user, err := s.queries.ValidateToken(nil, "token") + assert.ErrorIs(s.T(), err, ErrContextRequired) + assert.Nil(s.T(), user) +} + +func (s *AuthQueriesSuite) TestValidateToken_Success() { + user := domain.User{Active: true} + user.ID = 1 + s.userRepo.users[1] = user + + s.jwtManager.validateTokenFunc = func(tokenString string) (*auth.Claims, error) { + return &auth.Claims{UserID: 1}, nil + } + + retrievedUser, err := s.queries.ValidateToken(context.Background(), "valid-token") + assert.NoError(s.T(), err) + assert.NotNil(s.T(), retrievedUser) + assert.Equal(s.T(), user.ID, retrievedUser.ID) +} + +func (s *AuthQueriesSuite) TestValidateToken_EmptyToken() { + retrievedUser, err := s.queries.ValidateToken(context.Background(), "") + assert.ErrorIs(s.T(), err, auth.ErrMissingToken) + assert.Nil(s.T(), retrievedUser) +} + +func (s *AuthQueriesSuite) TestValidateToken_InvalidToken() { + s.jwtManager.validateTokenFunc = func(tokenString string) (*auth.Claims, error) { + return nil, errors.New("invalid token") + } + + retrievedUser, err := s.queries.ValidateToken(context.Background(), "invalid-token") + assert.Error(s.T(), err) + assert.Nil(s.T(), retrievedUser) +} + +func (s *AuthQueriesSuite) TestValidateToken_UserNotFound() { + s.jwtManager.validateTokenFunc = func(tokenString string) (*auth.Claims, error) { + return &auth.Claims{UserID: 1}, nil + } + + retrievedUser, err := s.queries.ValidateToken(context.Background(), "valid-token") + assert.ErrorIs(s.T(), err, ErrUserNotFound) + assert.Nil(s.T(), retrievedUser) +} + +func (s *AuthQueriesSuite) TestValidateToken_InactiveUser() { + user := domain.User{Active: false} + user.ID = 1 + s.userRepo.users[1] = user + + s.jwtManager.validateTokenFunc = func(tokenString string) (*auth.Claims, error) { + return &auth.Claims{UserID: 1}, nil + } + + retrievedUser, err := s.queries.ValidateToken(context.Background(), "valid-token") + assert.ErrorIs(s.T(), err, ErrInvalidCredentials) + assert.Nil(s.T(), retrievedUser) +} diff --git a/internal/app/copyright/commands.go b/internal/app/copyright/commands.go index 991de27..64c39dd 100644 --- a/internal/app/copyright/commands.go +++ b/internal/app/copyright/commands.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/log" ) // CopyrightCommands contains the command handlers for copyright. @@ -27,6 +28,7 @@ func (c *CopyrightCommands) CreateCopyright(ctx context.Context, copyright *doma if copyright.Identificator == "" { return errors.New("copyright identificator cannot be empty") } + log.LogDebug("Creating copyright", log.F("name", copyright.Name)) return c.repo.Create(ctx, copyright) } @@ -44,6 +46,7 @@ func (c *CopyrightCommands) UpdateCopyright(ctx context.Context, copyright *doma if copyright.Identificator == "" { return errors.New("copyright identificator cannot be empty") } + log.LogDebug("Updating copyright", log.F("id", copyright.ID)) return c.repo.Update(ctx, copyright) } @@ -52,15 +55,16 @@ func (c *CopyrightCommands) DeleteCopyright(ctx context.Context, id uint) error if id == 0 { return errors.New("invalid copyright ID") } + log.LogDebug("Deleting copyright", log.F("id", id)) return c.repo.Delete(ctx, id) } - // AddCopyrightToWork adds a copyright to a work. func (c *CopyrightCommands) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error { if workID == 0 || copyrightID == 0 { return errors.New("invalid work ID or copyright ID") } + log.LogDebug("Adding copyright to work", log.F("work_id", workID), log.F("copyright_id", copyrightID)) return c.repo.AddCopyrightToWork(ctx, workID, copyrightID) } @@ -69,6 +73,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromWork(ctx context.Context, workID if workID == 0 || copyrightID == 0 { return errors.New("invalid work ID or copyright ID") } + log.LogDebug("Removing copyright from work", log.F("work_id", workID), log.F("copyright_id", copyrightID)) return c.repo.RemoveCopyrightFromWork(ctx, workID, copyrightID) } @@ -77,6 +82,7 @@ func (c *CopyrightCommands) AddCopyrightToAuthor(ctx context.Context, authorID u if authorID == 0 || copyrightID == 0 { return errors.New("invalid author ID or copyright ID") } + log.LogDebug("Adding copyright to author", log.F("author_id", authorID), log.F("copyright_id", copyrightID)) return c.repo.AddCopyrightToAuthor(ctx, authorID, copyrightID) } @@ -85,6 +91,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromAuthor(ctx context.Context, autho if authorID == 0 || copyrightID == 0 { return errors.New("invalid author ID or copyright ID") } + log.LogDebug("Removing copyright from author", log.F("author_id", authorID), log.F("copyright_id", copyrightID)) return c.repo.RemoveCopyrightFromAuthor(ctx, authorID, copyrightID) } @@ -93,6 +100,7 @@ func (c *CopyrightCommands) AddCopyrightToBook(ctx context.Context, bookID uint, if bookID == 0 || copyrightID == 0 { return errors.New("invalid book ID or copyright ID") } + log.LogDebug("Adding copyright to book", log.F("book_id", bookID), log.F("copyright_id", copyrightID)) return c.repo.AddCopyrightToBook(ctx, bookID, copyrightID) } @@ -101,6 +109,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromBook(ctx context.Context, bookID if bookID == 0 || copyrightID == 0 { return errors.New("invalid book ID or copyright ID") } + log.LogDebug("Removing copyright from book", log.F("book_id", bookID), log.F("copyright_id", copyrightID)) return c.repo.RemoveCopyrightFromBook(ctx, bookID, copyrightID) } @@ -109,6 +118,7 @@ func (c *CopyrightCommands) AddCopyrightToPublisher(ctx context.Context, publish if publisherID == 0 || copyrightID == 0 { return errors.New("invalid publisher ID or copyright ID") } + log.LogDebug("Adding copyright to publisher", log.F("publisher_id", publisherID), log.F("copyright_id", copyrightID)) return c.repo.AddCopyrightToPublisher(ctx, publisherID, copyrightID) } @@ -117,6 +127,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromPublisher(ctx context.Context, pu if publisherID == 0 || copyrightID == 0 { return errors.New("invalid publisher ID or copyright ID") } + log.LogDebug("Removing copyright from publisher", log.F("publisher_id", publisherID), log.F("copyright_id", copyrightID)) return c.repo.RemoveCopyrightFromPublisher(ctx, publisherID, copyrightID) } @@ -125,6 +136,7 @@ func (c *CopyrightCommands) AddCopyrightToSource(ctx context.Context, sourceID u if sourceID == 0 || copyrightID == 0 { return errors.New("invalid source ID or copyright ID") } + log.LogDebug("Adding copyright to source", log.F("source_id", sourceID), log.F("copyright_id", copyrightID)) return c.repo.AddCopyrightToSource(ctx, sourceID, copyrightID) } @@ -133,6 +145,7 @@ func (c *CopyrightCommands) RemoveCopyrightFromSource(ctx context.Context, sourc if sourceID == 0 || copyrightID == 0 { return errors.New("invalid source ID or copyright ID") } + log.LogDebug("Removing copyright from source", log.F("source_id", sourceID), log.F("copyright_id", copyrightID)) return c.repo.RemoveCopyrightFromSource(ctx, sourceID, copyrightID) } @@ -150,5 +163,6 @@ func (c *CopyrightCommands) AddTranslation(ctx context.Context, translation *dom if translation.Message == "" { return errors.New("translation message cannot be empty") } + log.LogDebug("Adding translation to copyright", log.F("copyright_id", translation.CopyrightID), log.F("language", translation.LanguageCode)) return c.repo.AddTranslation(ctx, translation) } diff --git a/internal/app/copyright/commands_integration_test.go b/internal/app/copyright/commands_integration_test.go new file mode 100644 index 0000000..1c098a7 --- /dev/null +++ b/internal/app/copyright/commands_integration_test.go @@ -0,0 +1,239 @@ +//go:build integration + +package copyright_test + +import ( + "context" + "testing" + "tercul/internal/app/copyright" + "tercul/internal/domain" + "tercul/internal/testutil" + + "github.com/stretchr/testify/suite" +) + +type CopyrightCommandsTestSuite struct { + testutil.IntegrationTestSuite + commands *copyright.CopyrightCommands +} + +func (s *CopyrightCommandsTestSuite) SetupSuite() { + s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) + s.commands = copyright.NewCopyrightCommands(s.CopyrightRepo) +} + +func (s *CopyrightCommandsTestSuite) TestAddCopyrightToWork() { + s.Run("should add a copyright to a work", func() { + // Arrange + work := s.CreateTestWork("Test Work", "en", "Test content") + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + + // Act + err := s.commands.AddCopyrightToWork(context.Background(), work.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + + // Verify that the association was created in the database + var foundWork domain.Work + err = s.DB.Preload("Copyrights").First(&foundWork, work.ID).Error + s.Require().NoError(err) + s.Require().Len(foundWork.Copyrights, 1) + s.Equal(copyright.ID, foundWork.Copyrights[0].ID) + }) +} + +func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromWork() { + s.Run("should remove a copyright from a work", func() { + // Arrange + work := s.CreateTestWork("Test Work", "en", "Test content") + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + s.Require().NoError(s.commands.AddCopyrightToWork(context.Background(), work.ID, copyright.ID)) + + // Act + err := s.commands.RemoveCopyrightFromWork(context.Background(), work.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + + // Verify that the association was removed from the database + var foundWork domain.Work + err = s.DB.Preload("Copyrights").First(&foundWork, work.ID).Error + s.Require().NoError(err) + s.Require().Len(foundWork.Copyrights, 0) + }) +} + +func (s *CopyrightCommandsTestSuite) TestAddCopyrightToAuthor() { + s.Run("should add a copyright to an author", func() { + // Arrange + author := &domain.Author{Name: "Test Author"} + s.Require().NoError(s.AuthorRepo.Create(context.Background(), author)) + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + + // Act + err := s.commands.AddCopyrightToAuthor(context.Background(), author.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + var foundAuthor domain.Author + err = s.DB.Preload("Copyrights").First(&foundAuthor, author.ID).Error + s.Require().NoError(err) + s.Require().Len(foundAuthor.Copyrights, 1) + s.Equal(copyright.ID, foundAuthor.Copyrights[0].ID) + }) +} + +func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromAuthor() { + s.Run("should remove a copyright from an author", func() { + // Arrange + author := &domain.Author{Name: "Test Author"} + s.Require().NoError(s.AuthorRepo.Create(context.Background(), author)) + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + s.Require().NoError(s.commands.AddCopyrightToAuthor(context.Background(), author.ID, copyright.ID)) + + // Act + err := s.commands.RemoveCopyrightFromAuthor(context.Background(), author.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + var foundAuthor domain.Author + err = s.DB.Preload("Copyrights").First(&foundAuthor, author.ID).Error + s.Require().NoError(err) + s.Require().Len(foundAuthor.Copyrights, 0) + }) +} + +func (s *CopyrightCommandsTestSuite) TestAddCopyrightToBook() { + s.Run("should add a copyright to a book", func() { + // Arrange + book := &domain.Book{Title: "Test Book"} + s.Require().NoError(s.BookRepo.Create(context.Background(), book)) + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + + // Act + err := s.commands.AddCopyrightToBook(context.Background(), book.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + var foundBook domain.Book + err = s.DB.Preload("Copyrights").First(&foundBook, book.ID).Error + s.Require().NoError(err) + s.Require().Len(foundBook.Copyrights, 1) + s.Equal(copyright.ID, foundBook.Copyrights[0].ID) + }) +} + +func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromBook() { + s.Run("should remove a copyright from a book", func() { + // Arrange + book := &domain.Book{Title: "Test Book"} + s.Require().NoError(s.BookRepo.Create(context.Background(), book)) + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + s.Require().NoError(s.commands.AddCopyrightToBook(context.Background(), book.ID, copyright.ID)) + + // Act + err := s.commands.RemoveCopyrightFromBook(context.Background(), book.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + var foundBook domain.Book + err = s.DB.Preload("Copyrights").First(&foundBook, book.ID).Error + s.Require().NoError(err) + s.Require().Len(foundBook.Copyrights, 0) + }) +} + +func (s *CopyrightCommandsTestSuite) TestAddCopyrightToPublisher() { + s.Run("should add a copyright to a publisher", func() { + // Arrange + publisher := &domain.Publisher{Name: "Test Publisher"} + s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher)) + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + + // Act + err := s.commands.AddCopyrightToPublisher(context.Background(), publisher.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + var foundPublisher domain.Publisher + err = s.DB.Preload("Copyrights").First(&foundPublisher, publisher.ID).Error + s.Require().NoError(err) + s.Require().Len(foundPublisher.Copyrights, 1) + s.Equal(copyright.ID, foundPublisher.Copyrights[0].ID) + }) +} + +func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromPublisher() { + s.Run("should remove a copyright from a publisher", func() { + // Arrange + publisher := &domain.Publisher{Name: "Test Publisher"} + s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher)) + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + s.Require().NoError(s.commands.AddCopyrightToPublisher(context.Background(), publisher.ID, copyright.ID)) + + // Act + err := s.commands.RemoveCopyrightFromPublisher(context.Background(), publisher.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + var foundPublisher domain.Publisher + err = s.DB.Preload("Copyrights").First(&foundPublisher, publisher.ID).Error + s.Require().NoError(err) + s.Require().Len(foundPublisher.Copyrights, 0) + }) +} + +func (s *CopyrightCommandsTestSuite) TestAddCopyrightToSource() { + s.Run("should add a copyright to a source", func() { + // Arrange + source := &domain.Source{Name: "Test Source"} + s.Require().NoError(s.SourceRepo.Create(context.Background(), source)) + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + + // Act + err := s.commands.AddCopyrightToSource(context.Background(), source.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + var foundSource domain.Source + err = s.DB.Preload("Copyrights").First(&foundSource, source.ID).Error + s.Require().NoError(err) + s.Require().Len(foundSource.Copyrights, 1) + s.Equal(copyright.ID, foundSource.Copyrights[0].ID) + }) +} + +func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromSource() { + s.Run("should remove a copyright from a source", func() { + // Arrange + source := &domain.Source{Name: "Test Source"} + s.Require().NoError(s.SourceRepo.Create(context.Background(), source)) + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) + s.Require().NoError(s.commands.AddCopyrightToSource(context.Background(), source.ID, copyright.ID)) + + // Act + err := s.commands.RemoveCopyrightFromSource(context.Background(), source.ID, copyright.ID) + + // Assert + s.Require().NoError(err) + var foundSource domain.Source + err = s.DB.Preload("Copyrights").First(&foundSource, source.ID).Error + s.Require().NoError(err) + s.Require().Len(foundSource.Copyrights, 0) + }) +} + +func TestCopyrightCommands(t *testing.T) { + suite.Run(t, new(CopyrightCommandsTestSuite)) +} diff --git a/internal/app/copyright/commands_test.go b/internal/app/copyright/commands_test.go index 72f7402..67b46c6 100644 --- a/internal/app/copyright/commands_test.go +++ b/internal/app/copyright/commands_test.go @@ -1,237 +1,340 @@ -package copyright_test +package copyright import ( "context" - "testing" - "tercul/internal/app/copyright" - "tercul/internal/domain" - "tercul/internal/testutil" - + "errors" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "testing" ) -type CopyrightCommandsTestSuite struct { - testutil.IntegrationTestSuite - commands *copyright.CopyrightCommands +type CopyrightCommandsSuite struct { + suite.Suite + repo *mockCopyrightRepository + commands *CopyrightCommands } -func (s *CopyrightCommandsTestSuite) SetupSuite() { - s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) - s.commands = copyright.NewCopyrightCommands(s.CopyrightRepo) +func (s *CopyrightCommandsSuite) SetupTest() { + s.repo = &mockCopyrightRepository{} + s.commands = NewCopyrightCommands(s.repo) } -func (s *CopyrightCommandsTestSuite) TestAddCopyrightToWork() { - s.Run("should add a copyright to a work", func() { - // Arrange - work := s.CreateTestWork("Test Work", "en", "Test content") - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - - // Act - err := s.commands.AddCopyrightToWork(context.Background(), work.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - - // Verify that the association was created in the database - var foundWork domain.Work - err = s.DB.Preload("Copyrights").First(&foundWork, work.ID).Error - s.Require().NoError(err) - s.Require().Len(foundWork.Copyrights, 1) - s.Equal(copyright.ID, foundWork.Copyrights[0].ID) - }) +func TestCopyrightCommandsSuite(t *testing.T) { + suite.Run(t, new(CopyrightCommandsSuite)) } -func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromWork() { - s.Run("should remove a copyright from a work", func() { - // Arrange - work := s.CreateTestWork("Test Work", "en", "Test content") - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - s.Require().NoError(s.commands.AddCopyrightToWork(context.Background(), work.ID, copyright.ID)) - - // Act - err := s.commands.RemoveCopyrightFromWork(context.Background(), work.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - - // Verify that the association was removed from the database - var foundWork domain.Work - err = s.DB.Preload("Copyrights").First(&foundWork, work.ID).Error - s.Require().NoError(err) - s.Require().Len(foundWork.Copyrights, 0) - }) +func (s *CopyrightCommandsSuite) TestCreateCopyright_Success() { + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.repo.createFunc = func(ctx context.Context, c *domain.Copyright) error { + assert.Equal(s.T(), copyright.Name, c.Name) + return nil + } + err := s.commands.CreateCopyright(context.Background(), copyright) + assert.NoError(s.T(), err) } -func (s *CopyrightCommandsTestSuite) TestAddCopyrightToAuthor() { - s.Run("should add a copyright to an author", func() { - // Arrange - author := &domain.Author{Name: "Test Author"} - s.Require().NoError(s.AuthorRepo.Create(context.Background(), author)) - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - - // Act - err := s.commands.AddCopyrightToAuthor(context.Background(), author.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - var foundAuthor domain.Author - err = s.DB.Preload("Copyrights").First(&foundAuthor, author.ID).Error - s.Require().NoError(err) - s.Require().Len(foundAuthor.Copyrights, 1) - s.Equal(copyright.ID, foundAuthor.Copyrights[0].ID) - }) +func (s *CopyrightCommandsSuite) TestCreateCopyright_Nil() { + err := s.commands.CreateCopyright(context.Background(), nil) + assert.Error(s.T(), err) } -func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromAuthor() { - s.Run("should remove a copyright from an author", func() { - // Arrange - author := &domain.Author{Name: "Test Author"} - s.Require().NoError(s.AuthorRepo.Create(context.Background(), author)) - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - s.Require().NoError(s.commands.AddCopyrightToAuthor(context.Background(), author.ID, copyright.ID)) - - // Act - err := s.commands.RemoveCopyrightFromAuthor(context.Background(), author.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - var foundAuthor domain.Author - err = s.DB.Preload("Copyrights").First(&foundAuthor, author.ID).Error - s.Require().NoError(err) - s.Require().Len(foundAuthor.Copyrights, 0) - }) +func (s *CopyrightCommandsSuite) TestCreateCopyright_EmptyName() { + copyright := &domain.Copyright{Identificator: "TC-123"} + err := s.commands.CreateCopyright(context.Background(), copyright) + assert.Error(s.T(), err) } -func (s *CopyrightCommandsTestSuite) TestAddCopyrightToBook() { - s.Run("should add a copyright to a book", func() { - // Arrange - book := &domain.Book{Title: "Test Book"} - s.Require().NoError(s.BookRepo.Create(context.Background(), book)) - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - - // Act - err := s.commands.AddCopyrightToBook(context.Background(), book.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - var foundBook domain.Book - err = s.DB.Preload("Copyrights").First(&foundBook, book.ID).Error - s.Require().NoError(err) - s.Require().Len(foundBook.Copyrights, 1) - s.Equal(copyright.ID, foundBook.Copyrights[0].ID) - }) +func (s *CopyrightCommandsSuite) TestCreateCopyright_EmptyIdentificator() { + copyright := &domain.Copyright{Name: "Test Copyright"} + err := s.commands.CreateCopyright(context.Background(), copyright) + assert.Error(s.T(), err) } -func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromBook() { - s.Run("should remove a copyright from a book", func() { - // Arrange - book := &domain.Book{Title: "Test Book"} - s.Require().NoError(s.BookRepo.Create(context.Background(), book)) - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - s.Require().NoError(s.commands.AddCopyrightToBook(context.Background(), book.ID, copyright.ID)) - - // Act - err := s.commands.RemoveCopyrightFromBook(context.Background(), book.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - var foundBook domain.Book - err = s.DB.Preload("Copyrights").First(&foundBook, book.ID).Error - s.Require().NoError(err) - s.Require().Len(foundBook.Copyrights, 0) - }) +func (s *CopyrightCommandsSuite) TestCreateCopyright_RepoError() { + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + s.repo.createFunc = func(ctx context.Context, c *domain.Copyright) error { + return errors.New("db error") + } + err := s.commands.CreateCopyright(context.Background(), copyright) + assert.Error(s.T(), err) } -func (s *CopyrightCommandsTestSuite) TestAddCopyrightToPublisher() { - s.Run("should add a copyright to a publisher", func() { - // Arrange - publisher := &domain.Publisher{Name: "Test Publisher"} - s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher)) - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - - // Act - err := s.commands.AddCopyrightToPublisher(context.Background(), publisher.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - var foundPublisher domain.Publisher - err = s.DB.Preload("Copyrights").First(&foundPublisher, publisher.ID).Error - s.Require().NoError(err) - s.Require().Len(foundPublisher.Copyrights, 1) - s.Equal(copyright.ID, foundPublisher.Copyrights[0].ID) - }) +func (s *CopyrightCommandsSuite) TestUpdateCopyright_Success() { + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + copyright.ID = 1 + s.repo.updateFunc = func(ctx context.Context, c *domain.Copyright) error { + assert.Equal(s.T(), copyright.Name, c.Name) + return nil + } + err := s.commands.UpdateCopyright(context.Background(), copyright) + assert.NoError(s.T(), err) } -func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromPublisher() { - s.Run("should remove a copyright from a publisher", func() { - // Arrange - publisher := &domain.Publisher{Name: "Test Publisher"} - s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher)) - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - s.Require().NoError(s.commands.AddCopyrightToPublisher(context.Background(), publisher.ID, copyright.ID)) - - // Act - err := s.commands.RemoveCopyrightFromPublisher(context.Background(), publisher.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - var foundPublisher domain.Publisher - err = s.DB.Preload("Copyrights").First(&foundPublisher, publisher.ID).Error - s.Require().NoError(err) - s.Require().Len(foundPublisher.Copyrights, 0) - }) +func (s *CopyrightCommandsSuite) TestUpdateCopyright_Nil() { + err := s.commands.UpdateCopyright(context.Background(), nil) + assert.Error(s.T(), err) } -func (s *CopyrightCommandsTestSuite) TestAddCopyrightToSource() { - s.Run("should add a copyright to a source", func() { - // Arrange - source := &domain.Source{Name: "Test Source"} - s.Require().NoError(s.SourceRepo.Create(context.Background(), source)) - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - - // Act - err := s.commands.AddCopyrightToSource(context.Background(), source.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - var foundSource domain.Source - err = s.DB.Preload("Copyrights").First(&foundSource, source.ID).Error - s.Require().NoError(err) - s.Require().Len(foundSource.Copyrights, 1) - s.Equal(copyright.ID, foundSource.Copyrights[0].ID) - }) +func (s *CopyrightCommandsSuite) TestUpdateCopyright_ZeroID() { + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + err := s.commands.UpdateCopyright(context.Background(), copyright) + assert.Error(s.T(), err) } -func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromSource() { - s.Run("should remove a copyright from a source", func() { - // Arrange - source := &domain.Source{Name: "Test Source"} - s.Require().NoError(s.SourceRepo.Create(context.Background(), source)) - copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} - s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright)) - s.Require().NoError(s.commands.AddCopyrightToSource(context.Background(), source.ID, copyright.ID)) - - // Act - err := s.commands.RemoveCopyrightFromSource(context.Background(), source.ID, copyright.ID) - - // Assert - s.Require().NoError(err) - var foundSource domain.Source - err = s.DB.Preload("Copyrights").First(&foundSource, source.ID).Error - s.Require().NoError(err) - s.Require().Len(foundSource.Copyrights, 0) - }) +func (s *CopyrightCommandsSuite) TestUpdateCopyright_EmptyName() { + copyright := &domain.Copyright{Identificator: "TC-123"} + copyright.ID = 1 + err := s.commands.UpdateCopyright(context.Background(), copyright) + assert.Error(s.T(), err) } -func TestCopyrightCommands(t *testing.T) { - suite.Run(t, new(CopyrightCommandsTestSuite)) +func (s *CopyrightCommandsSuite) TestUpdateCopyright_EmptyIdentificator() { + copyright := &domain.Copyright{Name: "Test Copyright"} + copyright.ID = 1 + err := s.commands.UpdateCopyright(context.Background(), copyright) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestUpdateCopyright_RepoError() { + copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"} + copyright.ID = 1 + s.repo.updateFunc = func(ctx context.Context, c *domain.Copyright) error { + return errors.New("db error") + } + err := s.commands.UpdateCopyright(context.Background(), copyright) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestDeleteCopyright_Success() { + s.repo.deleteFunc = func(ctx context.Context, id uint) error { + assert.Equal(s.T(), uint(1), id) + return nil + } + err := s.commands.DeleteCopyright(context.Background(), 1) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestDeleteCopyright_ZeroID() { + err := s.commands.DeleteCopyright(context.Background(), 0) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestDeleteCopyright_RepoError() { + s.repo.deleteFunc = func(ctx context.Context, id uint) error { + return errors.New("db error") + } + err := s.commands.DeleteCopyright(context.Background(), 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToWork_Success() { + err := s.commands.AddCopyrightToWork(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToWork_ZeroID() { + err := s.commands.AddCopyrightToWork(context.Background(), 0, 2) + assert.Error(s.T(), err) + err = s.commands.AddCopyrightToWork(context.Background(), 1, 0) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromWork_Success() { + err := s.commands.RemoveCopyrightFromWork(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToWork_RepoError() { + s.repo.addCopyrightToWorkFunc = func(ctx context.Context, workID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.AddCopyrightToWork(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromWork_RepoError() { + s.repo.removeCopyrightFromWorkFunc = func(ctx context.Context, workID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveCopyrightFromWork(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromWork_ZeroID() { + err := s.commands.RemoveCopyrightFromWork(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToAuthor_ZeroID() { + err := s.commands.AddCopyrightToAuthor(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromAuthor_ZeroID() { + err := s.commands.RemoveCopyrightFromAuthor(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToBook_ZeroID() { + err := s.commands.AddCopyrightToBook(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromBook_ZeroID() { + err := s.commands.RemoveCopyrightFromBook(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToPublisher_ZeroID() { + err := s.commands.AddCopyrightToPublisher(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromPublisher_ZeroID() { + err := s.commands.RemoveCopyrightFromPublisher(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToSource_ZeroID() { + err := s.commands.AddCopyrightToSource(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromSource_ZeroID() { + err := s.commands.RemoveCopyrightFromSource(context.Background(), 0, 1) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToAuthor_RepoError() { + s.repo.addCopyrightToAuthorFunc = func(ctx context.Context, authorID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.AddCopyrightToAuthor(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromAuthor_RepoError() { + s.repo.removeCopyrightFromAuthorFunc = func(ctx context.Context, authorID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveCopyrightFromAuthor(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToBook_RepoError() { + s.repo.addCopyrightToBookFunc = func(ctx context.Context, bookID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.AddCopyrightToBook(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromBook_RepoError() { + s.repo.removeCopyrightFromBookFunc = func(ctx context.Context, bookID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveCopyrightFromBook(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToPublisher_RepoError() { + s.repo.addCopyrightToPublisherFunc = func(ctx context.Context, publisherID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.AddCopyrightToPublisher(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromPublisher_RepoError() { + s.repo.removeCopyrightFromPublisherFunc = func(ctx context.Context, publisherID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveCopyrightFromPublisher(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToSource_RepoError() { + s.repo.addCopyrightToSourceFunc = func(ctx context.Context, sourceID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.AddCopyrightToSource(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromSource_RepoError() { + s.repo.removeCopyrightFromSourceFunc = func(ctx context.Context, sourceID uint, copyrightID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveCopyrightFromSource(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToAuthor_Success() { + err := s.commands.AddCopyrightToAuthor(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromAuthor_Success() { + err := s.commands.RemoveCopyrightFromAuthor(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToBook_Success() { + err := s.commands.AddCopyrightToBook(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromBook_Success() { + err := s.commands.RemoveCopyrightFromBook(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToPublisher_Success() { + err := s.commands.AddCopyrightToPublisher(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromPublisher_Success() { + err := s.commands.RemoveCopyrightFromPublisher(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddCopyrightToSource_Success() { + err := s.commands.AddCopyrightToSource(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestRemoveCopyrightFromSource_Success() { + err := s.commands.RemoveCopyrightFromSource(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddTranslation_Success() { + translation := &domain.CopyrightTranslation{CopyrightID: 1, LanguageCode: "en", Message: "Test"} + err := s.commands.AddTranslation(context.Background(), translation) + assert.NoError(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddTranslation_Nil() { + err := s.commands.AddTranslation(context.Background(), nil) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddTranslation_ZeroCopyrightID() { + translation := &domain.CopyrightTranslation{LanguageCode: "en", Message: "Test"} + err := s.commands.AddTranslation(context.Background(), translation) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddTranslation_EmptyLanguageCode() { + translation := &domain.CopyrightTranslation{CopyrightID: 1, Message: "Test"} + err := s.commands.AddTranslation(context.Background(), translation) + assert.Error(s.T(), err) +} + +func (s *CopyrightCommandsSuite) TestAddTranslation_EmptyMessage() { + translation := &domain.CopyrightTranslation{CopyrightID: 1, LanguageCode: "en"} + err := s.commands.AddTranslation(context.Background(), translation) + assert.Error(s.T(), err) } diff --git a/internal/app/copyright/main_test.go b/internal/app/copyright/main_test.go new file mode 100644 index 0000000..79cef2a --- /dev/null +++ b/internal/app/copyright/main_test.go @@ -0,0 +1,223 @@ +package copyright + +import ( + "context" + "gorm.io/gorm" + "tercul/internal/domain" +) + +type mockCopyrightRepository struct { + createFunc func(ctx context.Context, copyright *domain.Copyright) error + updateFunc func(ctx context.Context, copyright *domain.Copyright) error + deleteFunc func(ctx context.Context, id uint) error + addCopyrightToWorkFunc func(ctx context.Context, workID uint, copyrightID uint) error + removeCopyrightFromWorkFunc func(ctx context.Context, workID uint, copyrightID uint) error + addCopyrightToAuthorFunc func(ctx context.Context, authorID uint, copyrightID uint) error + removeCopyrightFromAuthorFunc func(ctx context.Context, authorID uint, copyrightID uint) error + addCopyrightToBookFunc func(ctx context.Context, bookID uint, copyrightID uint) error + removeCopyrightFromBookFunc func(ctx context.Context, bookID uint, copyrightID uint) error + addCopyrightToPublisherFunc func(ctx context.Context, publisherID uint, copyrightID uint) error + removeCopyrightFromPublisherFunc func(ctx context.Context, publisherID uint, copyrightID uint) error + addCopyrightToSourceFunc func(ctx context.Context, sourceID uint, copyrightID uint) error + removeCopyrightFromSourceFunc func(ctx context.Context, sourceID uint, copyrightID uint) error + addTranslationFunc func(ctx context.Context, translation *domain.CopyrightTranslation) error + getByIDFunc func(ctx context.Context, id uint) (*domain.Copyright, error) + listAllFunc func(ctx context.Context) ([]domain.Copyright, error) + getTranslationsFunc func(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) + getTranslationByLanguageFunc func(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) +} + +func (m *mockCopyrightRepository) Create(ctx context.Context, copyright *domain.Copyright) error { + if m.createFunc != nil { + return m.createFunc(ctx, copyright) + } + return nil +} +func (m *mockCopyrightRepository) Update(ctx context.Context, copyright *domain.Copyright) error { + if m.updateFunc != nil { + return m.updateFunc(ctx, copyright) + } + return nil +} +func (m *mockCopyrightRepository) Delete(ctx context.Context, id uint) error { + if m.deleteFunc != nil { + return m.deleteFunc(ctx, id) + } + return nil +} +func (m *mockCopyrightRepository) AddCopyrightToWork(ctx context.Context, workID uint, copyrightID uint) error { + if m.addCopyrightToWorkFunc != nil { + return m.addCopyrightToWorkFunc(ctx, workID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) RemoveCopyrightFromWork(ctx context.Context, workID uint, copyrightID uint) error { + if m.removeCopyrightFromWorkFunc != nil { + return m.removeCopyrightFromWorkFunc(ctx, workID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) AddCopyrightToAuthor(ctx context.Context, authorID uint, copyrightID uint) error { + if m.addCopyrightToAuthorFunc != nil { + return m.addCopyrightToAuthorFunc(ctx, authorID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) RemoveCopyrightFromAuthor(ctx context.Context, authorID uint, copyrightID uint) error { + if m.removeCopyrightFromAuthorFunc != nil { + return m.removeCopyrightFromAuthorFunc(ctx, authorID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) AddCopyrightToBook(ctx context.Context, bookID uint, copyrightID uint) error { + if m.addCopyrightToBookFunc != nil { + return m.addCopyrightToBookFunc(ctx, bookID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) RemoveCopyrightFromBook(ctx context.Context, bookID uint, copyrightID uint) error { + if m.removeCopyrightFromBookFunc != nil { + return m.removeCopyrightFromBookFunc(ctx, bookID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) AddCopyrightToPublisher(ctx context.Context, publisherID uint, copyrightID uint) error { + if m.addCopyrightToPublisherFunc != nil { + return m.addCopyrightToPublisherFunc(ctx, publisherID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) RemoveCopyrightFromPublisher(ctx context.Context, publisherID uint, copyrightID uint) error { + if m.removeCopyrightFromPublisherFunc != nil { + return m.removeCopyrightFromPublisherFunc(ctx, publisherID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) AddCopyrightToSource(ctx context.Context, sourceID uint, copyrightID uint) error { + if m.addCopyrightToSourceFunc != nil { + return m.addCopyrightToSourceFunc(ctx, sourceID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) RemoveCopyrightFromSource(ctx context.Context, sourceID uint, copyrightID uint) error { + if m.removeCopyrightFromSourceFunc != nil { + return m.removeCopyrightFromSourceFunc(ctx, sourceID, copyrightID) + } + return nil +} +func (m *mockCopyrightRepository) AddTranslation(ctx context.Context, translation *domain.CopyrightTranslation) error { + if m.addTranslationFunc != nil { + return m.addTranslationFunc(ctx, translation) + } + return nil +} +func (m *mockCopyrightRepository) GetByID(ctx context.Context, id uint) (*domain.Copyright, error) { + if m.getByIDFunc != nil { + return m.getByIDFunc(ctx, id) + } + return nil, nil +} +func (m *mockCopyrightRepository) ListAll(ctx context.Context) ([]domain.Copyright, error) { + if m.listAllFunc != nil { + return m.listAllFunc(ctx) + } + return nil, nil +} +func (m *mockCopyrightRepository) GetTranslations(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) { + if m.getTranslationsFunc != nil { + return m.getTranslationsFunc(ctx, copyrightID) + } + return nil, nil +} +func (m *mockCopyrightRepository) GetTranslationByLanguage(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) { + if m.getTranslationByLanguageFunc != nil { + return m.getTranslationByLanguageFunc(ctx, copyrightID, languageCode) + } + return nil, nil +} + +// Implement the rest of the CopyrightRepository interface with empty methods. +func (m *mockCopyrightRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Copyright], error) { + return nil, nil +} +func (m *mockCopyrightRepository) Count(ctx context.Context) (int64, error) { return 0, nil } +func (m *mockCopyrightRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Copyright) error { + return nil +} +func (m *mockCopyrightRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Copyright, error) { + return nil, nil +} +func (m *mockCopyrightRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Copyright) error { + return nil +} +func (m *mockCopyrightRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { + return nil +} +func (m *mockCopyrightRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Copyright, error) { + return nil, nil +} +func (m *mockCopyrightRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { + return 0, nil +} +func (m *mockCopyrightRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Copyright, error) { + return nil, nil +} +func (m *mockCopyrightRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Copyright, error) { + return nil, nil +} +func (m *mockCopyrightRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil } +func (m *mockCopyrightRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil } +func (m *mockCopyrightRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { + return nil +} + +type mockWorkRepository struct { + domain.WorkRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) +} +func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} +type mockAuthorRepository struct { + domain.AuthorRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) +} +func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} +type mockBookRepository struct { + domain.BookRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) +} +func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} +type mockPublisherRepository struct { + domain.PublisherRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) +} +func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} +type mockSourceRepository struct { + domain.SourceRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) +} +func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} diff --git a/internal/app/copyright/queries.go b/internal/app/copyright/queries.go index b2660d6..ac29cdb 100644 --- a/internal/app/copyright/queries.go +++ b/internal/app/copyright/queries.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/log" ) // CopyrightQueries contains the query handlers for copyright. @@ -26,20 +27,21 @@ func (q *CopyrightQueries) GetCopyrightByID(ctx context.Context, id uint) (*doma if id == 0 { return nil, errors.New("invalid copyright ID") } + log.LogDebug("Getting copyright by ID", log.F("id", id)) return q.repo.GetByID(ctx, id) } // ListCopyrights retrieves all copyrights. func (q *CopyrightQueries) ListCopyrights(ctx context.Context) ([]domain.Copyright, error) { + log.LogDebug("Listing all copyrights") // Note: This might need pagination in the future. // For now, it mirrors the old service's behavior. return q.repo.ListAll(ctx) } - - // GetCopyrightsForWork gets all copyrights for a specific work. func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint) ([]*domain.Copyright, error) { + log.LogDebug("Getting copyrights for work", log.F("work_id", workID)) work, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) if err != nil { return nil, err @@ -49,6 +51,7 @@ func (q *CopyrightQueries) GetCopyrightsForWork(ctx context.Context, workID uint // GetCopyrightsForAuthor gets all copyrights for a specific author. func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID uint) ([]*domain.Copyright, error) { + log.LogDebug("Getting copyrights for author", log.F("author_id", authorID)) author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) if err != nil { return nil, err @@ -58,6 +61,7 @@ func (q *CopyrightQueries) GetCopyrightsForAuthor(ctx context.Context, authorID // GetCopyrightsForBook gets all copyrights for a specific book. func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint) ([]*domain.Copyright, error) { + log.LogDebug("Getting copyrights for book", log.F("book_id", bookID)) book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) if err != nil { return nil, err @@ -67,6 +71,7 @@ func (q *CopyrightQueries) GetCopyrightsForBook(ctx context.Context, bookID uint // GetCopyrightsForPublisher gets all copyrights for a specific publisher. func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Copyright, error) { + log.LogDebug("Getting copyrights for publisher", log.F("publisher_id", publisherID)) publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) if err != nil { return nil, err @@ -76,6 +81,7 @@ func (q *CopyrightQueries) GetCopyrightsForPublisher(ctx context.Context, publis // GetCopyrightsForSource gets all copyrights for a specific source. func (q *CopyrightQueries) GetCopyrightsForSource(ctx context.Context, sourceID uint) ([]*domain.Copyright, error) { + log.LogDebug("Getting copyrights for source", log.F("source_id", sourceID)) source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Copyrights"}}) if err != nil { return nil, err @@ -88,6 +94,7 @@ func (q *CopyrightQueries) GetTranslations(ctx context.Context, copyrightID uint if copyrightID == 0 { return nil, errors.New("invalid copyright ID") } + log.LogDebug("Getting translations for copyright", log.F("copyright_id", copyrightID)) return q.repo.GetTranslations(ctx, copyrightID) } @@ -99,5 +106,6 @@ func (q *CopyrightQueries) GetTranslationByLanguage(ctx context.Context, copyrig if languageCode == "" { return nil, errors.New("language code cannot be empty") } + log.LogDebug("Getting translation by language for copyright", log.F("copyright_id", copyrightID), log.F("language", languageCode)) return q.repo.GetTranslationByLanguage(ctx, copyrightID, languageCode) } diff --git a/internal/app/copyright/queries_test.go b/internal/app/copyright/queries_test.go new file mode 100644 index 0000000..5bb731c --- /dev/null +++ b/internal/app/copyright/queries_test.go @@ -0,0 +1,195 @@ +package copyright + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "testing" +) + +type CopyrightQueriesSuite struct { + suite.Suite + repo *mockCopyrightRepository + workRepo *mockWorkRepository + authorRepo *mockAuthorRepository + bookRepo *mockBookRepository + publisherRepo *mockPublisherRepository + sourceRepo *mockSourceRepository + queries *CopyrightQueries +} + +func (s *CopyrightQueriesSuite) SetupTest() { + s.repo = &mockCopyrightRepository{} + s.workRepo = &mockWorkRepository{} + s.authorRepo = &mockAuthorRepository{} + s.bookRepo = &mockBookRepository{} + s.publisherRepo = &mockPublisherRepository{} + s.sourceRepo = &mockSourceRepository{} + s.queries = NewCopyrightQueries(s.repo, s.workRepo, s.authorRepo, s.bookRepo, s.publisherRepo, s.sourceRepo) +} + +func TestCopyrightQueriesSuite(t *testing.T) { + suite.Run(t, new(CopyrightQueriesSuite)) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightByID_Success() { + copyright := &domain.Copyright{Name: "Test Copyright"} + copyright.ID = 1 + s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Copyright, error) { + return copyright, nil + } + c, err := s.queries.GetCopyrightByID(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), copyright, c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightByID_ZeroID() { + c, err := s.queries.GetCopyrightByID(context.Background(), 0) + assert.Error(s.T(), err) + assert.Nil(s.T(), c) +} + +func (s *CopyrightQueriesSuite) TestListCopyrights_Success() { + copyrights := []domain.Copyright{{Name: "Test Copyright"}} + s.repo.listAllFunc = func(ctx context.Context) ([]domain.Copyright, error) { + return copyrights, nil + } + c, err := s.queries.ListCopyrights(context.Background()) + assert.NoError(s.T(), err) + assert.Equal(s.T(), copyrights, c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForAuthor_RepoError() { + s.authorRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { + return nil, errors.New("db error") + } + c, err := s.queries.GetCopyrightsForAuthor(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForBook_RepoError() { + s.bookRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) { + return nil, errors.New("db error") + } + c, err := s.queries.GetCopyrightsForBook(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForPublisher_RepoError() { + s.publisherRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) { + return nil, errors.New("db error") + } + c, err := s.queries.GetCopyrightsForPublisher(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForSource_RepoError() { + s.sourceRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) { + return nil, errors.New("db error") + } + c, err := s.queries.GetCopyrightsForSource(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForWork_Success() { + copyrights := []*domain.Copyright{{Name: "Test Copyright"}} + s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { + return &domain.Work{Copyrights: copyrights}, nil + } + c, err := s.queries.GetCopyrightsForWork(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), copyrights, c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForWork_RepoError() { + s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { + return nil, errors.New("db error") + } + c, err := s.queries.GetCopyrightsForWork(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForAuthor_Success() { + copyrights := []*domain.Copyright{{Name: "Test Copyright"}} + s.authorRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { + return &domain.Author{Copyrights: copyrights}, nil + } + c, err := s.queries.GetCopyrightsForAuthor(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), copyrights, c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForBook_Success() { + copyrights := []*domain.Copyright{{Name: "Test Copyright"}} + s.bookRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) { + return &domain.Book{Copyrights: copyrights}, nil + } + c, err := s.queries.GetCopyrightsForBook(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), copyrights, c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForPublisher_Success() { + copyrights := []*domain.Copyright{{Name: "Test Copyright"}} + s.publisherRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) { + return &domain.Publisher{Copyrights: copyrights}, nil + } + c, err := s.queries.GetCopyrightsForPublisher(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), copyrights, c) +} + +func (s *CopyrightQueriesSuite) TestGetCopyrightsForSource_Success() { + copyrights := []*domain.Copyright{{Name: "Test Copyright"}} + s.sourceRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) { + return &domain.Source{Copyrights: copyrights}, nil + } + c, err := s.queries.GetCopyrightsForSource(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), copyrights, c) +} + +func (s *CopyrightQueriesSuite) TestGetTranslations_Success() { + translations := []domain.CopyrightTranslation{{Message: "Test"}} + s.repo.getTranslationsFunc = func(ctx context.Context, copyrightID uint) ([]domain.CopyrightTranslation, error) { + return translations, nil + } + t, err := s.queries.GetTranslations(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), translations, t) +} + +func (s *CopyrightQueriesSuite) TestGetTranslations_ZeroID() { + t, err := s.queries.GetTranslations(context.Background(), 0) + assert.Error(s.T(), err) + assert.Nil(s.T(), t) +} + +func (s *CopyrightQueriesSuite) TestGetTranslationByLanguage_Success() { + translation := &domain.CopyrightTranslation{Message: "Test"} + s.repo.getTranslationByLanguageFunc = func(ctx context.Context, copyrightID uint, languageCode string) (*domain.CopyrightTranslation, error) { + return translation, nil + } + t, err := s.queries.GetTranslationByLanguage(context.Background(), 1, "en") + assert.NoError(s.T(), err) + assert.Equal(s.T(), translation, t) +} + +func (s *CopyrightQueriesSuite) TestGetTranslationByLanguage_ZeroID() { + t, err := s.queries.GetTranslationByLanguage(context.Background(), 0, "en") + assert.Error(s.T(), err) + assert.Nil(s.T(), t) +} + +func (s *CopyrightQueriesSuite) TestGetTranslationByLanguage_EmptyLang() { + t, err := s.queries.GetTranslationByLanguage(context.Background(), 1, "") + assert.Error(s.T(), err) + assert.Nil(s.T(), t) +} diff --git a/internal/app/localization/service.go b/internal/app/localization/service.go index 9e1a428..108f4a4 100644 --- a/internal/app/localization/service.go +++ b/internal/app/localization/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/log" ) // Service resolves localized attributes using translations @@ -24,26 +25,32 @@ func (s *service) GetWorkContent(ctx context.Context, workID uint, preferredLang if workID == 0 { return "", errors.New("invalid work ID") } + log.LogDebug("fetching translations for work", log.F("work_id", workID)) translations, err := s.translationRepo.ListByWorkID(ctx, workID) if err != nil { + log.LogError("failed to fetch translations for work", log.F("work_id", workID), log.F("error", err)) return "", err } - return pickContent(translations, preferredLanguage), nil + return pickContent(ctx, translations, preferredLanguage), nil } func (s *service) GetAuthorBiography(ctx context.Context, authorID uint, preferredLanguage string) (string, error) { if authorID == 0 { return "", errors.New("invalid author ID") } + log.LogDebug("fetching translations for author", log.F("author_id", authorID)) translations, err := s.translationRepo.ListByEntity(ctx, "Author", authorID) if err != nil { + log.LogError("failed to fetch translations for author", log.F("author_id", authorID), log.F("error", err)) return "", err } + // Prefer Description from Translation as biography proxy var byLang *domain.Translation for i := range translations { tr := &translations[i] if tr.IsOriginalLanguage && tr.Description != "" { + log.LogDebug("found original language biography for author", log.F("author_id", authorID), log.F("language", tr.Language)) return tr.Description, nil } if tr.Language == preferredLanguage && byLang == nil && tr.Description != "" { @@ -51,22 +58,28 @@ func (s *service) GetAuthorBiography(ctx context.Context, authorID uint, preferr } } if byLang != nil { + log.LogDebug("found preferred language biography for author", log.F("author_id", authorID), log.F("language", byLang.Language)) return byLang.Description, nil } + // fallback to any non-empty description for i := range translations { if translations[i].Description != "" { + log.LogDebug("found fallback biography for author", log.F("author_id", authorID), log.F("language", translations[i].Language)) return translations[i].Description, nil } } + + log.LogDebug("no biography found for author", log.F("author_id", authorID)) return "", nil } -func pickContent(translations []domain.Translation, preferredLanguage string) string { +func pickContent(ctx context.Context, translations []domain.Translation, preferredLanguage string) string { var byLang *domain.Translation for i := range translations { tr := &translations[i] if tr.IsOriginalLanguage { + log.LogDebug("found original language content", log.F("language", tr.Language)) return tr.Content } if tr.Language == preferredLanguage && byLang == nil { @@ -74,10 +87,14 @@ func pickContent(translations []domain.Translation, preferredLanguage string) st } } if byLang != nil { + log.LogDebug("found preferred language content", log.F("language", byLang.Language)) return byLang.Content } if len(translations) > 0 { + log.LogDebug("found fallback content", log.F("language", translations[0].Language)) return translations[0].Content } + + log.LogDebug("no content found") return "" } diff --git a/internal/app/localization/service_test.go b/internal/app/localization/service_test.go new file mode 100644 index 0000000..73e1501 --- /dev/null +++ b/internal/app/localization/service_test.go @@ -0,0 +1,231 @@ +package localization + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "testing" + "gorm.io/gorm" +) + +// mockTranslationRepository is a local mock for the TranslationRepository interface. +type mockTranslationRepository struct { + translations []domain.Translation + err error +} + +func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { + if m.err != nil { + return nil, m.err + } + var results []domain.Translation + for _, t := range m.translations { + if t.TranslatableType == "Work" && t.TranslatableID == workID { + results = append(results, t) + } + } + return results, nil +} + +func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { + if m.err != nil { + return nil, m.err + } + var results []domain.Translation + for _, t := range m.translations { + if t.TranslatableType == entityType && t.TranslatableID == entityID { + results = append(results, t) + } + } + return results, nil +} + +// Implement the rest of the TranslationRepository interface with empty methods. +func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error { + m.translations = append(m.translations, *entity) + return nil +} +func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { return nil, 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) ListAll(ctx context.Context) ([]domain.Translation, error) { + return nil, nil +} +func (m *mockTranslationRepository) Count(ctx context.Context) (int64, error) { return 0, 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) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { + return nil +} +func (m *mockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) { + return nil, 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) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) { + return nil, nil +} +func (m *mockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { + return 0, nil +} +func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) { + return nil, nil +} +func (m *mockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) { + return nil, nil +} +func (m *mockTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) { + return false, 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 nil +} + +type LocalizationServiceSuite struct { + suite.Suite + repo *mockTranslationRepository + service Service +} + +func (s *LocalizationServiceSuite) SetupTest() { + s.repo = &mockTranslationRepository{} + s.service = NewService(s.repo) +} + +func TestLocalizationServiceSuite(t *testing.T) { + suite.Run(t, new(LocalizationServiceSuite)) +} + +func (s *LocalizationServiceSuite) TestGetWorkContent_ZeroWorkID() { + content, err := s.service.GetWorkContent(context.Background(), 0, "en") + assert.Error(s.T(), err) + assert.Equal(s.T(), "invalid work ID", err.Error()) + assert.Empty(s.T(), content) +} + +func (s *LocalizationServiceSuite) TestGetWorkContent_NoTranslations() { + content, err := s.service.GetWorkContent(context.Background(), 1, "en") + assert.NoError(s.T(), err) + assert.Empty(s.T(), content) +} + +func (s *LocalizationServiceSuite) TestGetWorkContent_OriginalLanguage() { + s.repo.translations = []domain.Translation{ + {TranslatableType: "Work", TranslatableID: 1, Language: "es", Content: "Contenido original", IsOriginalLanguage: true}, + {TranslatableType: "Work", TranslatableID: 1, Language: "en", Content: "English content", IsOriginalLanguage: false}, + } + + content, err := s.service.GetWorkContent(context.Background(), 1, "fr") + assert.NoError(s.T(), err) + assert.Equal(s.T(), "Contenido original", content) +} + +func (s *LocalizationServiceSuite) TestGetWorkContent_PreferredLanguage() { + s.repo.translations = []domain.Translation{ + {TranslatableType: "Work", TranslatableID: 1, Language: "es", Content: "Contenido en español", IsOriginalLanguage: false}, + {TranslatableType: "Work", TranslatableID: 1, Language: "en", Content: "English content", IsOriginalLanguage: false}, + } + + content, err := s.service.GetWorkContent(context.Background(), 1, "en") + assert.NoError(s.T(), err) + assert.Equal(s.T(), "English content", content) +} + +func (s *LocalizationServiceSuite) TestGetWorkContent_Fallback() { + s.repo.translations = []domain.Translation{ + {TranslatableType: "Work", TranslatableID: 1, Language: "es", Content: "Contenido en español", IsOriginalLanguage: false}, + {TranslatableType: "Work", TranslatableID: 1, Language: "fr", Content: "Contenu en français", IsOriginalLanguage: false}, + } + + content, err := s.service.GetWorkContent(context.Background(), 1, "en") + assert.NoError(s.T(), err) + assert.Equal(s.T(), "Contenido en español", content) +} + +func (s *LocalizationServiceSuite) TestGetWorkContent_RepoError() { + s.repo.err = errors.New("database error") + content, err := s.service.GetWorkContent(context.Background(), 1, "en") + assert.Error(s.T(), err) + assert.Equal(s.T(), "database error", err.Error()) + assert.Empty(s.T(), content) +} + +func (s *LocalizationServiceSuite) TestGetAuthorBiography_ZeroAuthorID() { + content, err := s.service.GetAuthorBiography(context.Background(), 0, "en") + assert.Error(s.T(), err) + assert.Equal(s.T(), "invalid author ID", err.Error()) + assert.Empty(s.T(), content) +} + +func (s *LocalizationServiceSuite) TestGetAuthorBiography_NoTranslations() { + content, err := s.service.GetAuthorBiography(context.Background(), 1, "en") + assert.NoError(s.T(), err) + assert.Empty(s.T(), content) +} + +func (s *LocalizationServiceSuite) TestGetAuthorBiography_OriginalLanguage() { + s.repo.translations = []domain.Translation{ + {TranslatableType: "Author", TranslatableID: 1, Language: "es", Description: "Biografía original", IsOriginalLanguage: true}, + {TranslatableType: "Author", TranslatableID: 1, Language: "en", Description: "English biography", IsOriginalLanguage: false}, + } + + content, err := s.service.GetAuthorBiography(context.Background(), 1, "fr") + assert.NoError(s.T(), err) + assert.Equal(s.T(), "Biografía original", content) +} + +func (s *LocalizationServiceSuite) TestGetAuthorBiography_PreferredLanguage() { + s.repo.translations = []domain.Translation{ + {TranslatableType: "Author", TranslatableID: 1, Language: "es", Description: "Biografía en español", IsOriginalLanguage: false}, + {TranslatableType: "Author", TranslatableID: 1, Language: "en", Description: "English biography", IsOriginalLanguage: false}, + } + + content, err := s.service.GetAuthorBiography(context.Background(), 1, "en") + assert.NoError(s.T(), err) + assert.Equal(s.T(), "English biography", content) +} + +func (s *LocalizationServiceSuite) TestGetAuthorBiography_Fallback() { + s.repo.translations = []domain.Translation{ + {TranslatableType: "Author", TranslatableID: 1, Language: "es", Description: "Biografía en español", IsOriginalLanguage: false}, + {TranslatableType: "Author", TranslatableID: 1, Language: "fr", Description: "Biographie en français", IsOriginalLanguage: false}, + } + + content, err := s.service.GetAuthorBiography(context.Background(), 1, "en") + assert.NoError(s.T(), err) + assert.Equal(s.T(), "Biografía en español", content) +} + +func (s *LocalizationServiceSuite) TestGetAuthorBiography_NoDescription() { + s.repo.translations = []domain.Translation{ + {TranslatableType: "Author", TranslatableID: 1, Language: "es", Content: "Contenido sin descripción", IsOriginalLanguage: false}, + } + + content, err := s.service.GetAuthorBiography(context.Background(), 1, "en") + assert.NoError(s.T(), err) + assert.Empty(s.T(), content) +} + +func (s *LocalizationServiceSuite) TestGetAuthorBiography_RepoError() { + s.repo.err = errors.New("database error") + content, err := s.service.GetAuthorBiography(context.Background(), 1, "en") + assert.Error(s.T(), err) + assert.Equal(s.T(), "database error", err.Error()) + assert.Empty(s.T(), content) +} diff --git a/internal/app/monetization/commands.go b/internal/app/monetization/commands.go index 939ee55..4b5405b 100644 --- a/internal/app/monetization/commands.go +++ b/internal/app/monetization/commands.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/log" ) // MonetizationCommands contains the command handlers for monetization. @@ -21,6 +22,7 @@ func (c *MonetizationCommands) AddMonetizationToWork(ctx context.Context, workID if workID == 0 || monetizationID == 0 { return errors.New("invalid work ID or monetization ID") } + log.LogDebug("Adding monetization to work", log.F("work_id", workID), log.F("monetization_id", monetizationID)) return c.repo.AddMonetizationToWork(ctx, workID, monetizationID) } @@ -29,6 +31,7 @@ func (c *MonetizationCommands) RemoveMonetizationFromWork(ctx context.Context, w if workID == 0 || monetizationID == 0 { return errors.New("invalid work ID or monetization ID") } + log.LogDebug("Removing monetization from work", log.F("work_id", workID), log.F("monetization_id", monetizationID)) return c.repo.RemoveMonetizationFromWork(ctx, workID, monetizationID) } @@ -36,6 +39,7 @@ func (c *MonetizationCommands) AddMonetizationToAuthor(ctx context.Context, auth if authorID == 0 || monetizationID == 0 { return errors.New("invalid author ID or monetization ID") } + log.LogDebug("Adding monetization to author", log.F("author_id", authorID), log.F("monetization_id", monetizationID)) return c.repo.AddMonetizationToAuthor(ctx, authorID, monetizationID) } @@ -43,6 +47,7 @@ func (c *MonetizationCommands) RemoveMonetizationFromAuthor(ctx context.Context, if authorID == 0 || monetizationID == 0 { return errors.New("invalid author ID or monetization ID") } + log.LogDebug("Removing monetization from author", log.F("author_id", authorID), log.F("monetization_id", monetizationID)) return c.repo.RemoveMonetizationFromAuthor(ctx, authorID, monetizationID) } @@ -50,6 +55,7 @@ func (c *MonetizationCommands) AddMonetizationToBook(ctx context.Context, bookID if bookID == 0 || monetizationID == 0 { return errors.New("invalid book ID or monetization ID") } + log.LogDebug("Adding monetization to book", log.F("book_id", bookID), log.F("monetization_id", monetizationID)) return c.repo.AddMonetizationToBook(ctx, bookID, monetizationID) } @@ -57,6 +63,7 @@ func (c *MonetizationCommands) RemoveMonetizationFromBook(ctx context.Context, b if bookID == 0 || monetizationID == 0 { return errors.New("invalid book ID or monetization ID") } + log.LogDebug("Removing monetization from book", log.F("book_id", bookID), log.F("monetization_id", monetizationID)) return c.repo.RemoveMonetizationFromBook(ctx, bookID, monetizationID) } @@ -64,6 +71,7 @@ func (c *MonetizationCommands) AddMonetizationToPublisher(ctx context.Context, p if publisherID == 0 || monetizationID == 0 { return errors.New("invalid publisher ID or monetization ID") } + log.LogDebug("Adding monetization to publisher", log.F("publisher_id", publisherID), log.F("monetization_id", monetizationID)) return c.repo.AddMonetizationToPublisher(ctx, publisherID, monetizationID) } @@ -71,6 +79,7 @@ func (c *MonetizationCommands) RemoveMonetizationFromPublisher(ctx context.Conte if publisherID == 0 || monetizationID == 0 { return errors.New("invalid publisher ID or monetization ID") } + log.LogDebug("Removing monetization from publisher", log.F("publisher_id", publisherID), log.F("monetization_id", monetizationID)) return c.repo.RemoveMonetizationFromPublisher(ctx, publisherID, monetizationID) } @@ -78,6 +87,7 @@ func (c *MonetizationCommands) AddMonetizationToSource(ctx context.Context, sour if sourceID == 0 || monetizationID == 0 { return errors.New("invalid source ID or monetization ID") } + log.LogDebug("Adding monetization to source", log.F("source_id", sourceID), log.F("monetization_id", monetizationID)) return c.repo.AddMonetizationToSource(ctx, sourceID, monetizationID) } @@ -85,5 +95,6 @@ func (c *MonetizationCommands) RemoveMonetizationFromSource(ctx context.Context, if sourceID == 0 || monetizationID == 0 { return errors.New("invalid source ID or monetization ID") } + log.LogDebug("Removing monetization from source", log.F("source_id", sourceID), log.F("monetization_id", monetizationID)) return c.repo.RemoveMonetizationFromSource(ctx, sourceID, monetizationID) } diff --git a/internal/app/monetization/commands_integration_test.go b/internal/app/monetization/commands_integration_test.go new file mode 100644 index 0000000..188886c --- /dev/null +++ b/internal/app/monetization/commands_integration_test.go @@ -0,0 +1,217 @@ +//go:build integration + +package monetization_test + +import ( + "context" + "testing" + "tercul/internal/app/monetization" + "tercul/internal/domain" + "tercul/internal/testutil" + + "github.com/stretchr/testify/suite" +) + +type MonetizationCommandsTestSuite struct { + testutil.IntegrationTestSuite + commands *monetization.MonetizationCommands +} + +func (s *MonetizationCommandsTestSuite) SetupSuite() { + s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) + s.commands = monetization.NewMonetizationCommands(s.MonetizationRepo) +} + +func (s *MonetizationCommandsTestSuite) TestAddMonetizationToWork() { + s.Run("should add a monetization to a work", func() { + // Arrange + work := s.CreateTestWork("Test Work", "en", "Test content") + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + + // Act + err := s.commands.AddMonetizationToWork(context.Background(), work.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + + // Verify that the association was created in the database + var foundWork domain.Work + err = s.DB.Preload("Monetizations").First(&foundWork, work.ID).Error + s.Require().NoError(err) + s.Require().Len(foundWork.Monetizations, 1) + s.Equal(monetization.ID, foundWork.Monetizations[0].ID) + }) +} + +func (s *MonetizationCommandsTestSuite) TestAddMonetizationToAuthor() { + s.Run("should add a monetization to an author", func() { + // Arrange + author := &domain.Author{Name: "Test Author"} + s.Require().NoError(s.AuthorRepo.Create(context.Background(), author)) + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + + // Act + err := s.commands.AddMonetizationToAuthor(context.Background(), author.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + var foundAuthor domain.Author + err = s.DB.Preload("Monetizations").First(&foundAuthor, author.ID).Error + s.Require().NoError(err) + s.Require().Len(foundAuthor.Monetizations, 1) + s.Equal(monetization.ID, foundAuthor.Monetizations[0].ID) + }) +} + +func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromAuthor() { + s.Run("should remove a monetization from an author", func() { + // Arrange + author := &domain.Author{Name: "Test Author"} + s.Require().NoError(s.AuthorRepo.Create(context.Background(), author)) + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + s.Require().NoError(s.commands.AddMonetizationToAuthor(context.Background(), author.ID, monetization.ID)) + + // Act + err := s.commands.RemoveMonetizationFromAuthor(context.Background(), author.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + var foundAuthor domain.Author + err = s.DB.Preload("Monetizations").First(&foundAuthor, author.ID).Error + s.Require().NoError(err) + s.Require().Len(foundAuthor.Monetizations, 0) + }) +} + +func (s *MonetizationCommandsTestSuite) TestAddMonetizationToBook() { + s.Run("should add a monetization to a book", func() { + // Arrange + book := &domain.Book{Title: "Test Book"} + s.Require().NoError(s.BookRepo.Create(context.Background(), book)) + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + + // Act + err := s.commands.AddMonetizationToBook(context.Background(), book.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + var foundBook domain.Book + err = s.DB.Preload("Monetizations").First(&foundBook, book.ID).Error + s.Require().NoError(err) + s.Require().Len(foundBook.Monetizations, 1) + s.Equal(monetization.ID, foundBook.Monetizations[0].ID) + }) +} + +func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromBook() { + s.Run("should remove a monetization from a book", func() { + // Arrange + book := &domain.Book{Title: "Test Book"} + s.Require().NoError(s.BookRepo.Create(context.Background(), book)) + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + s.Require().NoError(s.commands.AddMonetizationToBook(context.Background(), book.ID, monetization.ID)) + + // Act + err := s.commands.RemoveMonetizationFromBook(context.Background(), book.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + var foundBook domain.Book + err = s.DB.Preload("Monetizations").First(&foundBook, book.ID).Error + s.Require().NoError(err) + s.Require().Len(foundBook.Monetizations, 0) + }) +} + +func (s *MonetizationCommandsTestSuite) TestAddMonetizationToPublisher() { + s.Run("should add a monetization to a publisher", func() { + // Arrange + publisher := &domain.Publisher{Name: "Test Publisher"} + s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher)) + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + + // Act + err := s.commands.AddMonetizationToPublisher(context.Background(), publisher.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + var foundPublisher domain.Publisher + err = s.DB.Preload("Monetizations").First(&foundPublisher, publisher.ID).Error + s.Require().NoError(err) + s.Require().Len(foundPublisher.Monetizations, 1) + s.Equal(monetization.ID, foundPublisher.Monetizations[0].ID) + }) +} + +func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromPublisher() { + s.Run("should remove a monetization from a publisher", func() { + // Arrange + publisher := &domain.Publisher{Name: "Test Publisher"} + s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher)) + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + s.Require().NoError(s.commands.AddMonetizationToPublisher(context.Background(), publisher.ID, monetization.ID)) + + // Act + err := s.commands.RemoveMonetizationFromPublisher(context.Background(), publisher.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + var foundPublisher domain.Publisher + err = s.DB.Preload("Monetizations").First(&foundPublisher, publisher.ID).Error + s.Require().NoError(err) + s.Require().Len(foundPublisher.Monetizations, 0) + }) +} + +func (s *MonetizationCommandsTestSuite) TestAddMonetizationToSource() { + s.Run("should add a monetization to a source", func() { + // Arrange + source := &domain.Source{Name: "Test Source"} + s.Require().NoError(s.SourceRepo.Create(context.Background(), source)) + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + + // Act + err := s.commands.AddMonetizationToSource(context.Background(), source.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + var foundSource domain.Source + err = s.DB.Preload("Monetizations").First(&foundSource, source.ID).Error + s.Require().NoError(err) + s.Require().Len(foundSource.Monetizations, 1) + s.Equal(monetization.ID, foundSource.Monetizations[0].ID) + }) +} + +func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromSource() { + s.Run("should remove a monetization from a source", func() { + // Arrange + source := &domain.Source{Name: "Test Source"} + s.Require().NoError(s.SourceRepo.Create(context.Background(), source)) + monetization := &domain.Monetization{Amount: 10.0} + s.Require().NoError(s.DB.Create(monetization).Error) + s.Require().NoError(s.commands.AddMonetizationToSource(context.Background(), source.ID, monetization.ID)) + + // Act + err := s.commands.RemoveMonetizationFromSource(context.Background(), source.ID, monetization.ID) + + // Assert + s.Require().NoError(err) + var foundSource domain.Source + err = s.DB.Preload("Monetizations").First(&foundSource, source.ID).Error + s.Require().NoError(err) + s.Require().Len(foundSource.Monetizations, 0) + }) +} + +func TestMonetizationCommands(t *testing.T) { + suite.Run(t, new(MonetizationCommandsTestSuite)) +} diff --git a/internal/app/monetization/commands_test.go b/internal/app/monetization/commands_test.go index 58f9115..5e0adb0 100644 --- a/internal/app/monetization/commands_test.go +++ b/internal/app/monetization/commands_test.go @@ -1,215 +1,206 @@ -package monetization_test +package monetization import ( "context" - "testing" - "tercul/internal/app/monetization" - "tercul/internal/domain" - "tercul/internal/testutil" - + "errors" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "testing" ) -type MonetizationCommandsTestSuite struct { - testutil.IntegrationTestSuite - commands *monetization.MonetizationCommands +type MonetizationCommandsSuite struct { + suite.Suite + repo *mockMonetizationRepository + commands *MonetizationCommands } -func (s *MonetizationCommandsTestSuite) SetupSuite() { - s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig()) - s.commands = monetization.NewMonetizationCommands(s.MonetizationRepo) +func (s *MonetizationCommandsSuite) SetupTest() { + s.repo = &mockMonetizationRepository{} + s.commands = NewMonetizationCommands(s.repo) } -func (s *MonetizationCommandsTestSuite) TestAddMonetizationToWork() { - s.Run("should add a monetization to a work", func() { - // Arrange - work := s.CreateTestWork("Test Work", "en", "Test content") - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - - // Act - err := s.commands.AddMonetizationToWork(context.Background(), work.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - - // Verify that the association was created in the database - var foundWork domain.Work - err = s.DB.Preload("Monetizations").First(&foundWork, work.ID).Error - s.Require().NoError(err) - s.Require().Len(foundWork.Monetizations, 1) - s.Equal(monetization.ID, foundWork.Monetizations[0].ID) - }) +func TestMonetizationCommandsSuite(t *testing.T) { + suite.Run(t, new(MonetizationCommandsSuite)) } -func (s *MonetizationCommandsTestSuite) TestAddMonetizationToAuthor() { - s.Run("should add a monetization to an author", func() { - // Arrange - author := &domain.Author{Name: "Test Author"} - s.Require().NoError(s.AuthorRepo.Create(context.Background(), author)) - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - - // Act - err := s.commands.AddMonetizationToAuthor(context.Background(), author.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - var foundAuthor domain.Author - err = s.DB.Preload("Monetizations").First(&foundAuthor, author.ID).Error - s.Require().NoError(err) - s.Require().Len(foundAuthor.Monetizations, 1) - s.Equal(monetization.ID, foundAuthor.Monetizations[0].ID) - }) +func (s *MonetizationCommandsSuite) TestAddMonetizationToWork_Success() { + err := s.commands.AddMonetizationToWork(context.Background(), 1, 2) + assert.NoError(s.T(), err) } -func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromAuthor() { - s.Run("should remove a monetization from an author", func() { - // Arrange - author := &domain.Author{Name: "Test Author"} - s.Require().NoError(s.AuthorRepo.Create(context.Background(), author)) - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - s.Require().NoError(s.commands.AddMonetizationToAuthor(context.Background(), author.ID, monetization.ID)) - - // Act - err := s.commands.RemoveMonetizationFromAuthor(context.Background(), author.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - var foundAuthor domain.Author - err = s.DB.Preload("Monetizations").First(&foundAuthor, author.ID).Error - s.Require().NoError(err) - s.Require().Len(foundAuthor.Monetizations, 0) - }) +func (s *MonetizationCommandsSuite) TestAddMonetizationToWork_ZeroID() { + err := s.commands.AddMonetizationToWork(context.Background(), 0, 2) + assert.Error(s.T(), err) + err = s.commands.AddMonetizationToWork(context.Background(), 1, 0) + assert.Error(s.T(), err) } -func (s *MonetizationCommandsTestSuite) TestAddMonetizationToBook() { - s.Run("should add a monetization to a book", func() { - // Arrange - book := &domain.Book{Title: "Test Book"} - s.Require().NoError(s.BookRepo.Create(context.Background(), book)) - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - - // Act - err := s.commands.AddMonetizationToBook(context.Background(), book.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - var foundBook domain.Book - err = s.DB.Preload("Monetizations").First(&foundBook, book.ID).Error - s.Require().NoError(err) - s.Require().Len(foundBook.Monetizations, 1) - s.Equal(monetization.ID, foundBook.Monetizations[0].ID) - }) +func (s *MonetizationCommandsSuite) TestAddMonetizationToWork_RepoError() { + s.repo.addMonetizationToWorkFunc = func(ctx context.Context, workID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.AddMonetizationToWork(context.Background(), 1, 2) + assert.Error(s.T(), err) } -func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromBook() { - s.Run("should remove a monetization from a book", func() { - // Arrange - book := &domain.Book{Title: "Test Book"} - s.Require().NoError(s.BookRepo.Create(context.Background(), book)) - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - s.Require().NoError(s.commands.AddMonetizationToBook(context.Background(), book.ID, monetization.ID)) - - // Act - err := s.commands.RemoveMonetizationFromBook(context.Background(), book.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - var foundBook domain.Book - err = s.DB.Preload("Monetizations").First(&foundBook, book.ID).Error - s.Require().NoError(err) - s.Require().Len(foundBook.Monetizations, 0) - }) +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromWork_Success() { + err := s.commands.RemoveMonetizationFromWork(context.Background(), 1, 2) + assert.NoError(s.T(), err) } -func (s *MonetizationCommandsTestSuite) TestAddMonetizationToPublisher() { - s.Run("should add a monetization to a publisher", func() { - // Arrange - publisher := &domain.Publisher{Name: "Test Publisher"} - s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher)) - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - - // Act - err := s.commands.AddMonetizationToPublisher(context.Background(), publisher.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - var foundPublisher domain.Publisher - err = s.DB.Preload("Monetizations").First(&foundPublisher, publisher.ID).Error - s.Require().NoError(err) - s.Require().Len(foundPublisher.Monetizations, 1) - s.Equal(monetization.ID, foundPublisher.Monetizations[0].ID) - }) +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromWork_ZeroID() { + err := s.commands.RemoveMonetizationFromWork(context.Background(), 0, 2) + assert.Error(s.T(), err) } -func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromPublisher() { - s.Run("should remove a monetization from a publisher", func() { - // Arrange - publisher := &domain.Publisher{Name: "Test Publisher"} - s.Require().NoError(s.PublisherRepo.Create(context.Background(), publisher)) - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - s.Require().NoError(s.commands.AddMonetizationToPublisher(context.Background(), publisher.ID, monetization.ID)) - - // Act - err := s.commands.RemoveMonetizationFromPublisher(context.Background(), publisher.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - var foundPublisher domain.Publisher - err = s.DB.Preload("Monetizations").First(&foundPublisher, publisher.ID).Error - s.Require().NoError(err) - s.Require().Len(foundPublisher.Monetizations, 0) - }) +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromWork_RepoError() { + s.repo.removeMonetizationFromWorkFunc = func(ctx context.Context, workID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveMonetizationFromWork(context.Background(), 1, 2) + assert.Error(s.T(), err) } -func (s *MonetizationCommandsTestSuite) TestAddMonetizationToSource() { - s.Run("should add a monetization to a source", func() { - // Arrange - source := &domain.Source{Name: "Test Source"} - s.Require().NoError(s.SourceRepo.Create(context.Background(), source)) - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - - // Act - err := s.commands.AddMonetizationToSource(context.Background(), source.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - var foundSource domain.Source - err = s.DB.Preload("Monetizations").First(&foundSource, source.ID).Error - s.Require().NoError(err) - s.Require().Len(foundSource.Monetizations, 1) - s.Equal(monetization.ID, foundSource.Monetizations[0].ID) - }) +func (s *MonetizationCommandsSuite) TestAddMonetizationToAuthor_Success() { + err := s.commands.AddMonetizationToAuthor(context.Background(), 1, 2) + assert.NoError(s.T(), err) } -func (s *MonetizationCommandsTestSuite) TestRemoveMonetizationFromSource() { - s.Run("should remove a monetization from a source", func() { - // Arrange - source := &domain.Source{Name: "Test Source"} - s.Require().NoError(s.SourceRepo.Create(context.Background(), source)) - monetization := &domain.Monetization{Amount: 10.0} - s.Require().NoError(s.DB.Create(monetization).Error) - s.Require().NoError(s.commands.AddMonetizationToSource(context.Background(), source.ID, monetization.ID)) - - // Act - err := s.commands.RemoveMonetizationFromSource(context.Background(), source.ID, monetization.ID) - - // Assert - s.Require().NoError(err) - var foundSource domain.Source - err = s.DB.Preload("Monetizations").First(&foundSource, source.ID).Error - s.Require().NoError(err) - s.Require().Len(foundSource.Monetizations, 0) - }) +func (s *MonetizationCommandsSuite) TestAddMonetizationToAuthor_ZeroID() { + err := s.commands.AddMonetizationToAuthor(context.Background(), 0, 2) + assert.Error(s.T(), err) } -func TestMonetizationCommands(t *testing.T) { - suite.Run(t, new(MonetizationCommandsTestSuite)) +func (s *MonetizationCommandsSuite) TestAddMonetizationToAuthor_RepoError() { + s.repo.addMonetizationToAuthorFunc = func(ctx context.Context, authorID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.AddMonetizationToAuthor(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromAuthor_Success() { + err := s.commands.RemoveMonetizationFromAuthor(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromAuthor_ZeroID() { + err := s.commands.RemoveMonetizationFromAuthor(context.Background(), 0, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromAuthor_RepoError() { + s.repo.removeMonetizationFromAuthorFunc = func(ctx context.Context, authorID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveMonetizationFromAuthor(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToBook_Success() { + err := s.commands.AddMonetizationToBook(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToBook_ZeroID() { + err := s.commands.AddMonetizationToBook(context.Background(), 0, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToBook_RepoError() { + s.repo.addMonetizationToBookFunc = func(ctx context.Context, bookID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.AddMonetizationToBook(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromBook_Success() { + err := s.commands.RemoveMonetizationFromBook(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromBook_ZeroID() { + err := s.commands.RemoveMonetizationFromBook(context.Background(), 0, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromBook_RepoError() { + s.repo.removeMonetizationFromBookFunc = func(ctx context.Context, bookID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveMonetizationFromBook(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToPublisher_Success() { + err := s.commands.AddMonetizationToPublisher(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToPublisher_ZeroID() { + err := s.commands.AddMonetizationToPublisher(context.Background(), 0, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToPublisher_RepoError() { + s.repo.addMonetizationToPublisherFunc = func(ctx context.Context, publisherID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.AddMonetizationToPublisher(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromPublisher_Success() { + err := s.commands.RemoveMonetizationFromPublisher(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromPublisher_ZeroID() { + err := s.commands.RemoveMonetizationFromPublisher(context.Background(), 0, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromPublisher_RepoError() { + s.repo.removeMonetizationFromPublisherFunc = func(ctx context.Context, publisherID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveMonetizationFromPublisher(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToSource_Success() { + err := s.commands.AddMonetizationToSource(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToSource_ZeroID() { + err := s.commands.AddMonetizationToSource(context.Background(), 0, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestAddMonetizationToSource_RepoError() { + s.repo.addMonetizationToSourceFunc = func(ctx context.Context, sourceID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.AddMonetizationToSource(context.Background(), 1, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromSource_Success() { + err := s.commands.RemoveMonetizationFromSource(context.Background(), 1, 2) + assert.NoError(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromSource_ZeroID() { + err := s.commands.RemoveMonetizationFromSource(context.Background(), 0, 2) + assert.Error(s.T(), err) +} + +func (s *MonetizationCommandsSuite) TestRemoveMonetizationFromSource_RepoError() { + s.repo.removeMonetizationFromSourceFunc = func(ctx context.Context, sourceID uint, monetizationID uint) error { + return errors.New("db error") + } + err := s.commands.RemoveMonetizationFromSource(context.Background(), 1, 2) + assert.Error(s.T(), err) } diff --git a/internal/app/monetization/main_test.go b/internal/app/monetization/main_test.go new file mode 100644 index 0000000..2ebb668 --- /dev/null +++ b/internal/app/monetization/main_test.go @@ -0,0 +1,148 @@ +package monetization + +import ( + "context" + "tercul/internal/domain" +) + +type mockMonetizationRepository struct { + domain.MonetizationRepository + addMonetizationToWorkFunc func(ctx context.Context, workID uint, monetizationID uint) error + removeMonetizationFromWorkFunc func(ctx context.Context, workID uint, monetizationID uint) error + addMonetizationToAuthorFunc func(ctx context.Context, authorID uint, monetizationID uint) error + removeMonetizationFromAuthorFunc func(ctx context.Context, authorID uint, monetizationID uint) error + addMonetizationToBookFunc func(ctx context.Context, bookID uint, monetizationID uint) error + removeMonetizationFromBookFunc func(ctx context.Context, bookID uint, monetizationID uint) error + addMonetizationToPublisherFunc func(ctx context.Context, publisherID uint, monetizationID uint) error + removeMonetizationFromPublisherFunc func(ctx context.Context, publisherID uint, monetizationID uint) error + addMonetizationToSourceFunc func(ctx context.Context, sourceID uint, monetizationID uint) error + removeMonetizationFromSourceFunc func(ctx context.Context, sourceID uint, monetizationID uint) error + getByIDFunc func(ctx context.Context, id uint) (*domain.Monetization, error) + listAllFunc func(ctx context.Context) ([]domain.Monetization, error) +} + +func (m *mockMonetizationRepository) GetByID(ctx context.Context, id uint) (*domain.Monetization, error) { + if m.getByIDFunc != nil { + return m.getByIDFunc(ctx, id) + } + return nil, nil +} + +func (m *mockMonetizationRepository) ListAll(ctx context.Context) ([]domain.Monetization, error) { + if m.listAllFunc != nil { + return m.listAllFunc(ctx) + } + return nil, nil +} + +func (m *mockMonetizationRepository) AddMonetizationToWork(ctx context.Context, workID uint, monetizationID uint) error { + if m.addMonetizationToWorkFunc != nil { + return m.addMonetizationToWorkFunc(ctx, workID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) RemoveMonetizationFromWork(ctx context.Context, workID uint, monetizationID uint) error { + if m.removeMonetizationFromWorkFunc != nil { + return m.removeMonetizationFromWorkFunc(ctx, workID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) AddMonetizationToAuthor(ctx context.Context, authorID uint, monetizationID uint) error { + if m.addMonetizationToAuthorFunc != nil { + return m.addMonetizationToAuthorFunc(ctx, authorID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) RemoveMonetizationFromAuthor(ctx context.Context, authorID uint, monetizationID uint) error { + if m.removeMonetizationFromAuthorFunc != nil { + return m.removeMonetizationFromAuthorFunc(ctx, authorID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) AddMonetizationToBook(ctx context.Context, bookID uint, monetizationID uint) error { + if m.addMonetizationToBookFunc != nil { + return m.addMonetizationToBookFunc(ctx, bookID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) RemoveMonetizationFromBook(ctx context.Context, bookID uint, monetizationID uint) error { + if m.removeMonetizationFromBookFunc != nil { + return m.removeMonetizationFromBookFunc(ctx, bookID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) AddMonetizationToPublisher(ctx context.Context, publisherID uint, monetizationID uint) error { + if m.addMonetizationToPublisherFunc != nil { + return m.addMonetizationToPublisherFunc(ctx, publisherID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) RemoveMonetizationFromPublisher(ctx context.Context, publisherID uint, monetizationID uint) error { + if m.removeMonetizationFromPublisherFunc != nil { + return m.removeMonetizationFromPublisherFunc(ctx, publisherID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) AddMonetizationToSource(ctx context.Context, sourceID uint, monetizationID uint) error { + if m.addMonetizationToSourceFunc != nil { + return m.addMonetizationToSourceFunc(ctx, sourceID, monetizationID) + } + return nil +} +func (m *mockMonetizationRepository) RemoveMonetizationFromSource(ctx context.Context, sourceID uint, monetizationID uint) error { + if m.removeMonetizationFromSourceFunc != nil { + return m.removeMonetizationFromSourceFunc(ctx, sourceID, monetizationID) + } + return nil +} + +type mockWorkRepository struct { + domain.WorkRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) +} +func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} +type mockAuthorRepository struct { + domain.AuthorRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) +} +func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} +type mockBookRepository struct { + domain.BookRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) +} +func (m *mockBookRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} +type mockPublisherRepository struct { + domain.PublisherRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) +} +func (m *mockPublisherRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} +type mockSourceRepository struct { + domain.SourceRepository + getByIDWithOptionsFunc func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) +} +func (m *mockSourceRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) { + if m.getByIDWithOptionsFunc != nil { + return m.getByIDWithOptionsFunc(ctx, id, options) + } + return nil, nil +} diff --git a/internal/app/monetization/queries.go b/internal/app/monetization/queries.go index 4e5f57f..4e1b410 100644 --- a/internal/app/monetization/queries.go +++ b/internal/app/monetization/queries.go @@ -4,6 +4,7 @@ import ( "context" "errors" "tercul/internal/domain" + "tercul/internal/platform/log" ) // MonetizationQueries contains the query handlers for monetization. @@ -26,15 +27,18 @@ func (q *MonetizationQueries) GetMonetizationByID(ctx context.Context, id uint) if id == 0 { return nil, errors.New("invalid monetization ID") } + log.LogDebug("Getting monetization by ID", log.F("id", id)) return q.repo.GetByID(ctx, id) } // ListMonetizations retrieves all monetizations. func (q *MonetizationQueries) ListMonetizations(ctx context.Context) ([]domain.Monetization, error) { + log.LogDebug("Listing all monetizations") return q.repo.ListAll(ctx) } func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workID uint) ([]*domain.Monetization, error) { + log.LogDebug("Getting monetizations for work", log.F("work_id", workID)) work, err := q.workRepo.GetByIDWithOptions(ctx, workID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) if err != nil { return nil, err @@ -43,6 +47,7 @@ func (q *MonetizationQueries) GetMonetizationsForWork(ctx context.Context, workI } func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, authorID uint) ([]*domain.Monetization, error) { + log.LogDebug("Getting monetizations for author", log.F("author_id", authorID)) author, err := q.authorRepo.GetByIDWithOptions(ctx, authorID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) if err != nil { return nil, err @@ -51,6 +56,7 @@ func (q *MonetizationQueries) GetMonetizationsForAuthor(ctx context.Context, aut } func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookID uint) ([]*domain.Monetization, error) { + log.LogDebug("Getting monetizations for book", log.F("book_id", bookID)) book, err := q.bookRepo.GetByIDWithOptions(ctx, bookID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) if err != nil { return nil, err @@ -59,6 +65,7 @@ func (q *MonetizationQueries) GetMonetizationsForBook(ctx context.Context, bookI } func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, publisherID uint) ([]*domain.Monetization, error) { + log.LogDebug("Getting monetizations for publisher", log.F("publisher_id", publisherID)) publisher, err := q.publisherRepo.GetByIDWithOptions(ctx, publisherID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) if err != nil { return nil, err @@ -67,6 +74,7 @@ func (q *MonetizationQueries) GetMonetizationsForPublisher(ctx context.Context, } func (q *MonetizationQueries) GetMonetizationsForSource(ctx context.Context, sourceID uint) ([]*domain.Monetization, error) { + log.LogDebug("Getting monetizations for source", log.F("source_id", sourceID)) source, err := q.sourceRepo.GetByIDWithOptions(ctx, sourceID, &domain.QueryOptions{Preloads: []string{"Monetizations"}}) if err != nil { return nil, err diff --git a/internal/app/monetization/queries_test.go b/internal/app/monetization/queries_test.go new file mode 100644 index 0000000..09cb1fa --- /dev/null +++ b/internal/app/monetization/queries_test.go @@ -0,0 +1,157 @@ +package monetization + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "testing" +) + +type MonetizationQueriesSuite struct { + suite.Suite + repo *mockMonetizationRepository + workRepo *mockWorkRepository + authorRepo *mockAuthorRepository + bookRepo *mockBookRepository + publisherRepo *mockPublisherRepository + sourceRepo *mockSourceRepository + queries *MonetizationQueries +} + +func (s *MonetizationQueriesSuite) SetupTest() { + s.repo = &mockMonetizationRepository{} + s.workRepo = &mockWorkRepository{} + s.authorRepo = &mockAuthorRepository{} + s.bookRepo = &mockBookRepository{} + s.publisherRepo = &mockPublisherRepository{} + s.sourceRepo = &mockSourceRepository{} + s.queries = NewMonetizationQueries(s.repo, s.workRepo, s.authorRepo, s.bookRepo, s.publisherRepo, s.sourceRepo) +} + +func TestMonetizationQueriesSuite(t *testing.T) { + suite.Run(t, new(MonetizationQueriesSuite)) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationByID_Success() { + monetization := &domain.Monetization{Amount: 10.0} + monetization.ID = 1 + s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Monetization, error) { + return monetization, nil + } + m, err := s.queries.GetMonetizationByID(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), monetization, m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationByID_ZeroID() { + m, err := s.queries.GetMonetizationByID(context.Background(), 0) + assert.Error(s.T(), err) + assert.Nil(s.T(), m) +} + +func (s *MonetizationQueriesSuite) TestListMonetizations_Success() { + monetizations := []domain.Monetization{{Amount: 10.0}} + s.repo.listAllFunc = func(ctx context.Context) ([]domain.Monetization, error) { + return monetizations, nil + } + m, err := s.queries.ListMonetizations(context.Background()) + assert.NoError(s.T(), err) + assert.Equal(s.T(), monetizations, m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForWork_Success() { + monetizations := []*domain.Monetization{{Amount: 10.0}} + s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { + return &domain.Work{Monetizations: monetizations}, nil + } + m, err := s.queries.GetMonetizationsForWork(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), monetizations, m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForWork_RepoError() { + s.workRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { + return nil, errors.New("db error") + } + m, err := s.queries.GetMonetizationsForWork(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForAuthor_Success() { + monetizations := []*domain.Monetization{{Amount: 10.0}} + s.authorRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { + return &domain.Author{Monetizations: monetizations}, nil + } + m, err := s.queries.GetMonetizationsForAuthor(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), monetizations, m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForAuthor_RepoError() { + s.authorRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { + return nil, errors.New("db error") + } + m, err := s.queries.GetMonetizationsForAuthor(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForBook_Success() { + monetizations := []*domain.Monetization{{Amount: 10.0}} + s.bookRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) { + return &domain.Book{Monetizations: monetizations}, nil + } + m, err := s.queries.GetMonetizationsForBook(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), monetizations, m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForBook_RepoError() { + s.bookRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Book, error) { + return nil, errors.New("db error") + } + m, err := s.queries.GetMonetizationsForBook(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForPublisher_Success() { + monetizations := []*domain.Monetization{{Amount: 10.0}} + s.publisherRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) { + return &domain.Publisher{Monetizations: monetizations}, nil + } + m, err := s.queries.GetMonetizationsForPublisher(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), monetizations, m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForPublisher_RepoError() { + s.publisherRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Publisher, error) { + return nil, errors.New("db error") + } + m, err := s.queries.GetMonetizationsForPublisher(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForSource_Success() { + monetizations := []*domain.Monetization{{Amount: 10.0}} + s.sourceRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) { + return &domain.Source{Monetizations: monetizations}, nil + } + m, err := s.queries.GetMonetizationsForSource(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), monetizations, m) +} + +func (s *MonetizationQueriesSuite) TestGetMonetizationsForSource_RepoError() { + s.sourceRepo.getByIDWithOptionsFunc = func(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Source, error) { + return nil, errors.New("db error") + } + m, err := s.queries.GetMonetizationsForSource(context.Background(), 1) + assert.Error(s.T(), err) + assert.Nil(s.T(), m) +} diff --git a/internal/app/search/service.go b/internal/app/search/service.go index 17440d8..d204b5d 100644 --- a/internal/app/search/service.go +++ b/internal/app/search/service.go @@ -3,9 +3,9 @@ package search import ( "context" "fmt" - "log" "tercul/internal/app/localization" "tercul/internal/domain" + "tercul/internal/platform/log" "tercul/internal/platform/search" ) @@ -16,39 +16,28 @@ type IndexService interface { type indexService struct { localization localization.Service - translations domain.TranslationRepository + weaviate search.WeaviateWrapper } -func NewIndexService(localization localization.Service, translations domain.TranslationRepository) IndexService { - return &indexService{localization: localization, translations: translations} +func NewIndexService(localization localization.Service, weaviate search.WeaviateWrapper) IndexService { + return &indexService{localization: localization, weaviate: weaviate} } func (s *indexService) IndexWork(ctx context.Context, work domain.Work) error { + log.LogDebug("Indexing work", log.F("work_id", work.ID)) // Choose best content snapshot for indexing content, err := s.localization.GetWorkContent(ctx, work.ID, work.Language) if err != nil { + log.LogError("Failed to get work content for indexing", log.F("work_id", work.ID), log.F("error", err)) return err } - props := map[string]interface{}{ - "language": work.Language, - "title": work.Title, - "description": work.Description, - "status": work.Status, - } - if content != "" { - props["content"] = content - } - - _, wErr := search.Client.Data().Creator(). - WithClassName("Work"). - WithID(formatID(work.ID)). - WithProperties(props). - Do(ctx) - if wErr != nil { - log.Printf("weaviate index error: %v", wErr) - return wErr + err = s.weaviate.IndexWork(ctx, &work, content) + if err != nil { + log.LogError("Failed to index work in Weaviate", log.F("work_id", work.ID), log.F("error", err)) + return err } + log.LogInfo("Successfully indexed work", log.F("work_id", work.ID)) return nil } diff --git a/internal/app/search/service_test.go b/internal/app/search/service_test.go new file mode 100644 index 0000000..213f725 --- /dev/null +++ b/internal/app/search/service_test.go @@ -0,0 +1,93 @@ +package search + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "testing" +) + +type mockLocalizationService struct { + getWorkContentFunc func(ctx context.Context, workID uint, preferredLanguage string) (string, error) +} + +func (m *mockLocalizationService) GetWorkContent(ctx context.Context, workID uint, preferredLanguage string) (string, error) { + if m.getWorkContentFunc != nil { + return m.getWorkContentFunc(ctx, workID, preferredLanguage) + } + return "", nil +} +func (m *mockLocalizationService) GetAuthorBiography(ctx context.Context, authorID uint, preferredLanguage string) (string, error) { + return "", nil +} + +type mockWeaviateWrapper struct { + indexWorkFunc func(ctx context.Context, work *domain.Work, content string) error +} + +func (m *mockWeaviateWrapper) IndexWork(ctx context.Context, work *domain.Work, content string) error { + if m.indexWorkFunc != nil { + return m.indexWorkFunc(ctx, work, content) + } + return nil +} + +type SearchServiceSuite struct { + suite.Suite + localization *mockLocalizationService + weaviate *mockWeaviateWrapper + service IndexService +} + +func (s *SearchServiceSuite) SetupTest() { + s.localization = &mockLocalizationService{} + s.weaviate = &mockWeaviateWrapper{} + s.service = NewIndexService(s.localization, s.weaviate) +} + +func TestSearchServiceSuite(t *testing.T) { + suite.Run(t, new(SearchServiceSuite)) +} + +func (s *SearchServiceSuite) TestIndexWork_Success() { + work := domain.Work{Title: "Test Work"} + work.ID = 1 + s.localization.getWorkContentFunc = func(ctx context.Context, workID uint, preferredLanguage string) (string, error) { + return "test content", nil + } + s.weaviate.indexWorkFunc = func(ctx context.Context, work *domain.Work, content string) error { + assert.Equal(s.T(), "test content", content) + return nil + } + err := s.service.IndexWork(context.Background(), work) + assert.NoError(s.T(), err) +} + +func (s *SearchServiceSuite) TestIndexWork_LocalizationError() { + work := domain.Work{Title: "Test Work"} + work.ID = 1 + s.localization.getWorkContentFunc = func(ctx context.Context, workID uint, preferredLanguage string) (string, error) { + return "", errors.New("localization error") + } + err := s.service.IndexWork(context.Background(), work) + assert.Error(s.T(), err) +} + +func TestFormatID(t *testing.T) { + assert.Equal(t, "123", formatID(123)) +} + +func (s *SearchServiceSuite) TestIndexWork_WeaviateError() { + work := domain.Work{Title: "Test Work"} + work.ID = 1 + s.localization.getWorkContentFunc = func(ctx context.Context, workID uint, preferredLanguage string) (string, error) { + return "test content", nil + } + s.weaviate.indexWorkFunc = func(ctx context.Context, work *domain.Work, content string) error { + return errors.New("weaviate error") + } + err := s.service.IndexWork(context.Background(), work) + assert.Error(s.T(), err) +} diff --git a/internal/app/work/commands.go b/internal/app/work/commands.go index 8eacec8..2bf7b80 100644 --- a/internal/app/work/commands.go +++ b/internal/app/work/commands.go @@ -6,18 +6,19 @@ import ( "tercul/internal/domain" ) +// Analyzer defines the interface for work analysis operations. +type Analyzer interface { + AnalyzeWork(ctx context.Context, workID uint) error +} + // WorkCommands contains the command handlers for the work aggregate. type WorkCommands struct { repo domain.WorkRepository - analyzer interface { // This will be replaced with a proper interface later - AnalyzeWork(ctx context.Context, workID uint) error - } + analyzer Analyzer } // NewWorkCommands creates a new WorkCommands handler. -func NewWorkCommands(repo domain.WorkRepository, analyzer interface { - AnalyzeWork(ctx context.Context, workID uint) error -}) *WorkCommands { +func NewWorkCommands(repo domain.WorkRepository, analyzer Analyzer) *WorkCommands { return &WorkCommands{ repo: repo, analyzer: analyzer, diff --git a/internal/app/work/commands_test.go b/internal/app/work/commands_test.go new file mode 100644 index 0000000..5821764 --- /dev/null +++ b/internal/app/work/commands_test.go @@ -0,0 +1,137 @@ +package work + +import ( + "context" + "errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "testing" +) + +type WorkCommandsSuite struct { + suite.Suite + repo *mockWorkRepository + analyzer *mockAnalyzer + commands *WorkCommands +} + +func (s *WorkCommandsSuite) SetupTest() { + s.repo = &mockWorkRepository{} + s.analyzer = &mockAnalyzer{} + s.commands = NewWorkCommands(s.repo, s.analyzer) +} + +func TestWorkCommandsSuite(t *testing.T) { + suite.Run(t, new(WorkCommandsSuite)) +} + +func (s *WorkCommandsSuite) TestCreateWork_Success() { + work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} + err := s.commands.CreateWork(context.Background(), work) + assert.NoError(s.T(), err) +} + +func (s *WorkCommandsSuite) TestCreateWork_Nil() { + err := s.commands.CreateWork(context.Background(), nil) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestCreateWork_EmptyTitle() { + work := &domain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}} + err := s.commands.CreateWork(context.Background(), work) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestCreateWork_EmptyLanguage() { + work := &domain.Work{Title: "Test Work"} + err := s.commands.CreateWork(context.Background(), work) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestCreateWork_RepoError() { + work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} + s.repo.createFunc = func(ctx context.Context, w *domain.Work) error { + return errors.New("db error") + } + err := s.commands.CreateWork(context.Background(), work) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestUpdateWork_Success() { + work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} + work.ID = 1 + err := s.commands.UpdateWork(context.Background(), work) + assert.NoError(s.T(), err) +} + +func (s *WorkCommandsSuite) TestUpdateWork_Nil() { + err := s.commands.UpdateWork(context.Background(), nil) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestUpdateWork_ZeroID() { + work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} + err := s.commands.UpdateWork(context.Background(), work) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestUpdateWork_EmptyTitle() { + work := &domain.Work{TranslatableModel: domain.TranslatableModel{Language: "en"}} + work.ID = 1 + err := s.commands.UpdateWork(context.Background(), work) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestUpdateWork_EmptyLanguage() { + work := &domain.Work{Title: "Test Work"} + work.ID = 1 + err := s.commands.UpdateWork(context.Background(), work) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestUpdateWork_RepoError() { + work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}} + work.ID = 1 + s.repo.updateFunc = func(ctx context.Context, w *domain.Work) error { + return errors.New("db error") + } + err := s.commands.UpdateWork(context.Background(), work) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestDeleteWork_Success() { + err := s.commands.DeleteWork(context.Background(), 1) + assert.NoError(s.T(), err) +} + +func (s *WorkCommandsSuite) TestDeleteWork_ZeroID() { + err := s.commands.DeleteWork(context.Background(), 0) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestDeleteWork_RepoError() { + s.repo.deleteFunc = func(ctx context.Context, id uint) error { + return errors.New("db error") + } + err := s.commands.DeleteWork(context.Background(), 1) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestAnalyzeWork_Success() { + err := s.commands.AnalyzeWork(context.Background(), 1) + assert.NoError(s.T(), err) +} + +func (s *WorkCommandsSuite) TestAnalyzeWork_ZeroID() { + err := s.commands.AnalyzeWork(context.Background(), 0) + assert.Error(s.T(), err) +} + +func (s *WorkCommandsSuite) TestAnalyzeWork_AnalyzerError() { + s.analyzer.analyzeWorkFunc = func(ctx context.Context, workID uint) error { + return errors.New("analyzer error") + } + err := s.commands.AnalyzeWork(context.Background(), 1) + assert.Error(s.T(), err) +} diff --git a/internal/app/work/main_test.go b/internal/app/work/main_test.go new file mode 100644 index 0000000..a28735c --- /dev/null +++ b/internal/app/work/main_test.go @@ -0,0 +1,92 @@ +package work + +import ( + "context" + "tercul/internal/domain" +) + +type mockWorkRepository struct { + domain.WorkRepository + createFunc func(ctx context.Context, work *domain.Work) error + updateFunc func(ctx context.Context, work *domain.Work) error + deleteFunc func(ctx context.Context, id uint) error + getByIDFunc func(ctx context.Context, id uint) (*domain.Work, error) + listFunc func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) + getWithTranslationsFunc func(ctx context.Context, id uint) (*domain.Work, error) + findByTitleFunc func(ctx context.Context, title string) ([]domain.Work, error) + findByAuthorFunc func(ctx context.Context, authorID uint) ([]domain.Work, error) + findByCategoryFunc func(ctx context.Context, categoryID uint) ([]domain.Work, error) + findByLanguageFunc func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) +} + +func (m *mockWorkRepository) Create(ctx context.Context, work *domain.Work) error { + if m.createFunc != nil { + return m.createFunc(ctx, work) + } + return nil +} +func (m *mockWorkRepository) Update(ctx context.Context, work *domain.Work) error { + if m.updateFunc != nil { + return m.updateFunc(ctx, work) + } + return nil +} +func (m *mockWorkRepository) Delete(ctx context.Context, id uint) error { + if m.deleteFunc != nil { + return m.deleteFunc(ctx, id) + } + return nil +} +func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) { + if m.getByIDFunc != nil { + return m.getByIDFunc(ctx, id) + } + return nil, nil +} +func (m *mockWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { + if m.listFunc != nil { + return m.listFunc(ctx, page, pageSize) + } + return nil, nil +} +func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) { + if m.getWithTranslationsFunc != nil { + return m.getWithTranslationsFunc(ctx, id) + } + return nil, nil +} +func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) { + if m.findByTitleFunc != nil { + return m.findByTitleFunc(ctx, title) + } + return nil, nil +} +func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) { + if m.findByAuthorFunc != nil { + return m.findByAuthorFunc(ctx, authorID) + } + return nil, nil +} +func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) { + if m.findByCategoryFunc != nil { + return m.findByCategoryFunc(ctx, categoryID) + } + return nil, nil +} +func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { + if m.findByLanguageFunc != nil { + return m.findByLanguageFunc(ctx, language, page, pageSize) + } + return nil, nil +} + +type mockAnalyzer struct { + analyzeWorkFunc func(ctx context.Context, workID uint) error +} + +func (m *mockAnalyzer) AnalyzeWork(ctx context.Context, workID uint) error { + if m.analyzeWorkFunc != nil { + return m.analyzeWorkFunc(ctx, workID) + } + return nil +} diff --git a/internal/app/work/queries_test.go b/internal/app/work/queries_test.go new file mode 100644 index 0000000..3a4d585 --- /dev/null +++ b/internal/app/work/queries_test.go @@ -0,0 +1,132 @@ +package work + +import ( + "context" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" + "tercul/internal/domain" + "testing" +) + +type WorkQueriesSuite struct { + suite.Suite + repo *mockWorkRepository + queries *WorkQueries +} + +func (s *WorkQueriesSuite) SetupTest() { + s.repo = &mockWorkRepository{} + s.queries = NewWorkQueries(s.repo) +} + +func TestWorkQueriesSuite(t *testing.T) { + suite.Run(t, new(WorkQueriesSuite)) +} + +func (s *WorkQueriesSuite) TestGetWorkByID_Success() { + work := &domain.Work{Title: "Test Work"} + work.ID = 1 + s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Work, error) { + return work, nil + } + w, err := s.queries.GetWorkByID(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), work, w) +} + +func (s *WorkQueriesSuite) TestGetWorkByID_ZeroID() { + w, err := s.queries.GetWorkByID(context.Background(), 0) + assert.Error(s.T(), err) + assert.Nil(s.T(), w) +} + +func (s *WorkQueriesSuite) TestListWorks_Success() { + works := &domain.PaginatedResult[domain.Work]{} + s.repo.listFunc = func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { + return works, nil + } + w, err := s.queries.ListWorks(context.Background(), 1, 10) + assert.NoError(s.T(), err) + assert.Equal(s.T(), works, w) +} + +func (s *WorkQueriesSuite) TestGetWorkWithTranslations_Success() { + work := &domain.Work{Title: "Test Work"} + work.ID = 1 + s.repo.getWithTranslationsFunc = func(ctx context.Context, id uint) (*domain.Work, error) { + return work, nil + } + w, err := s.queries.GetWorkWithTranslations(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), work, w) +} + +func (s *WorkQueriesSuite) TestGetWorkWithTranslations_ZeroID() { + w, err := s.queries.GetWorkWithTranslations(context.Background(), 0) + assert.Error(s.T(), err) + assert.Nil(s.T(), w) +} + +func (s *WorkQueriesSuite) TestFindWorksByTitle_Success() { + works := []domain.Work{{Title: "Test Work"}} + s.repo.findByTitleFunc = func(ctx context.Context, title string) ([]domain.Work, error) { + return works, nil + } + w, err := s.queries.FindWorksByTitle(context.Background(), "Test") + assert.NoError(s.T(), err) + assert.Equal(s.T(), works, w) +} + +func (s *WorkQueriesSuite) TestFindWorksByTitle_Empty() { + w, err := s.queries.FindWorksByTitle(context.Background(), "") + assert.Error(s.T(), err) + assert.Nil(s.T(), w) +} + +func (s *WorkQueriesSuite) TestFindWorksByAuthor_Success() { + works := []domain.Work{{Title: "Test Work"}} + s.repo.findByAuthorFunc = func(ctx context.Context, authorID uint) ([]domain.Work, error) { + return works, nil + } + w, err := s.queries.FindWorksByAuthor(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), works, w) +} + +func (s *WorkQueriesSuite) TestFindWorksByAuthor_ZeroID() { + w, err := s.queries.FindWorksByAuthor(context.Background(), 0) + assert.Error(s.T(), err) + assert.Nil(s.T(), w) +} + +func (s *WorkQueriesSuite) TestFindWorksByCategory_Success() { + works := []domain.Work{{Title: "Test Work"}} + s.repo.findByCategoryFunc = func(ctx context.Context, categoryID uint) ([]domain.Work, error) { + return works, nil + } + w, err := s.queries.FindWorksByCategory(context.Background(), 1) + assert.NoError(s.T(), err) + assert.Equal(s.T(), works, w) +} + +func (s *WorkQueriesSuite) TestFindWorksByCategory_ZeroID() { + w, err := s.queries.FindWorksByCategory(context.Background(), 0) + assert.Error(s.T(), err) + assert.Nil(s.T(), w) +} + +func (s *WorkQueriesSuite) TestFindWorksByLanguage_Success() { + works := &domain.PaginatedResult[domain.Work]{} + s.repo.findByLanguageFunc = func(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { + return works, nil + } + w, err := s.queries.FindWorksByLanguage(context.Background(), "en", 1, 10) + assert.NoError(s.T(), err) + assert.Equal(s.T(), works, w) +} + +func (s *WorkQueriesSuite) TestFindWorksByLanguage_Empty() { + w, err := s.queries.FindWorksByLanguage(context.Background(), "", 1, 10) + assert.Error(s.T(), err) + assert.Nil(s.T(), w) +} diff --git a/internal/platform/auth/jwt.go b/internal/platform/auth/jwt.go index 0f87264..1738cf3 100644 --- a/internal/platform/auth/jwt.go +++ b/internal/platform/auth/jwt.go @@ -29,6 +29,11 @@ type Claims struct { } // JWTManager handles JWT token operations +type JWTManagement interface { + GenerateToken(user *domain.User) (string, error) + ValidateToken(tokenString string) (*Claims, error) +} + type JWTManager struct { secretKey []byte issuer string diff --git a/internal/platform/search/weaviate_client.go b/internal/platform/search/weaviate_client.go index a40a4de..44c1fc2 100644 --- a/internal/platform/search/weaviate_client.go +++ b/internal/platform/search/weaviate_client.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "tercul/internal/domain" - "tercul/internal/platform/config" "time" "github.com/weaviate/weaviate-go-client/v5/weaviate" @@ -13,21 +12,8 @@ import ( var Client *weaviate.Client -func InitWeaviate() { - var err error - Client, err = weaviate.NewClient(weaviate.Config{ - Scheme: "http", - Host: config.Cfg.WeaviateHost, - }) - if err != nil { - log.Fatalf("Failed to connect to Weaviate: %v", err) - } - - log.Println("Connected to Weaviate successfully.") -} - // UpsertWork inserts or updates a Work object in Weaviate -func UpsertWork(work domain.Work) error { +func UpsertWork(client *weaviate.Client, work domain.Work) error { // Create a properties map with the fields that exist in the Work model properties := map[string]interface{}{ "language": work.Language, @@ -39,7 +25,7 @@ func UpsertWork(work domain.Work) error { "updatedAt": work.UpdatedAt.Format(time.RFC3339), } - _, err := Client.Data().Creator(). + _, err := client.Data().Creator(). WithClassName("Work"). WithID(fmt.Sprintf("%d", work.ID)). // Use the ID from the Work model WithProperties(properties). diff --git a/internal/platform/search/weaviate_wrapper.go b/internal/platform/search/weaviate_wrapper.go new file mode 100644 index 0000000..20563ce --- /dev/null +++ b/internal/platform/search/weaviate_wrapper.go @@ -0,0 +1,44 @@ +package search + +import ( + "context" + "fmt" + "tercul/internal/domain" + "time" + + "github.com/weaviate/weaviate-go-client/v5/weaviate" +) + +type WeaviateWrapper interface { + IndexWork(ctx context.Context, work *domain.Work, content string) error +} + +type weaviateWrapper struct { + client *weaviate.Client +} + +func NewWeaviateWrapper(client *weaviate.Client) WeaviateWrapper { + return &weaviateWrapper{client: client} +} + +func (w *weaviateWrapper) IndexWork(ctx context.Context, work *domain.Work, content string) error { + properties := map[string]interface{}{ + "language": work.Language, + "title": work.Title, + "description": work.Description, + "status": work.Status, + "createdAt": work.CreatedAt.Format(time.RFC3339), + "updatedAt": work.UpdatedAt.Format(time.RFC3339), + } + if content != "" { + properties["content"] = content + } + + _, err := w.client.Data().Creator(). + WithClassName("Work"). + WithID(fmt.Sprintf("%d", work.ID)). + WithProperties(properties). + Do(ctx) + + return err +}