mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 00:31:35 +00:00
Merge pull request #24 from SamyRai/test/increase-coverage-and-fix-bugs
feat(testing): Increase Test Coverage and Fix Authorization Bugs
This commit is contained in:
commit
d189a009da
@ -132,7 +132,7 @@ func main() {
|
||||
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
|
||||
localizationService := localization.NewService(repos.Localization)
|
||||
searchService := appsearch.NewService(searchClient, localizationService)
|
||||
authzService := authz.NewService(repos.Work, repos.Translation)
|
||||
authzService := authz.NewService(repos.Work, repos.Author, repos.User, repos.Translation)
|
||||
authorService := author.NewService(repos.Author)
|
||||
bookService := book.NewService(repos.Book, authzService)
|
||||
bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService)
|
||||
@ -146,7 +146,7 @@ func main() {
|
||||
translationService := translation.NewService(repos.Translation, authzService)
|
||||
userService := user.NewService(repos.User, authzService, repos.UserProfile)
|
||||
authService := auth.NewService(repos.User, jwtManager)
|
||||
workService := work.NewService(repos.Work, searchClient, authzService, analyticsService)
|
||||
workService := work.NewService(repos.Work, repos.Author, repos.User, searchClient, authzService, analyticsService)
|
||||
|
||||
// Create application
|
||||
application := app.NewApplication(
|
||||
|
||||
131
internal/adapters/graphql/author_resolvers_test.go
Normal file
131
internal/adapters/graphql/author_resolvers_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type AuthorResolversTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
queryResolver graphql.QueryResolver
|
||||
mutationResolver graphql.MutationResolver
|
||||
}
|
||||
|
||||
func TestAuthorResolvers(t *testing.T) {
|
||||
suite.Run(t, new(AuthorResolversTestSuite))
|
||||
}
|
||||
|
||||
func (s *AuthorResolversTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(&testutil.TestConfig{
|
||||
DBPath: "author_resolvers_test.db",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthorResolversTestSuite) TearDownSuite() {
|
||||
s.IntegrationTestSuite.TearDownSuite()
|
||||
os.Remove("author_resolvers_test.db")
|
||||
}
|
||||
|
||||
func (s *AuthorResolversTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
resolver := &graphql.Resolver{App: s.App}
|
||||
s.queryResolver = resolver.Query()
|
||||
s.mutationResolver = resolver.Mutation()
|
||||
}
|
||||
|
||||
// Helper to create a user for tests
|
||||
func (s *AuthorResolversTestSuite) createUser(username, email, password string, role domain.UserRole) *domain.User {
|
||||
resp, err := s.App.Auth.Commands.Register(context.Background(), auth.RegisterInput{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
user, err := s.App.User.Queries.User(context.Background(), resp.User.ID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
if role != user.Role {
|
||||
user.Role = role
|
||||
err = s.DB.Save(user).Error
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// Helper to create a context with JWT claims
|
||||
func (s *AuthorResolversTestSuite) contextWithClaims(user *domain.User) context.Context {
|
||||
return testutil.ContextWithClaims(context.Background(), &platform_auth.Claims{
|
||||
UserID: user.ID,
|
||||
Role: string(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthorResolversTestSuite) TestAuthorMutations() {
|
||||
user := s.createUser("author-creator", "author-creator@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
var authorID string
|
||||
|
||||
s.Run("Create Author", func() {
|
||||
input := model.AuthorInput{
|
||||
Name: "J.R.R. Tolkien",
|
||||
}
|
||||
author, err := s.mutationResolver.CreateAuthor(ctx, input)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(author)
|
||||
s.Equal("J.R.R. Tolkien", author.Name)
|
||||
authorID = author.ID
|
||||
})
|
||||
|
||||
s.Run("Update Author", func() {
|
||||
input := model.AuthorInput{
|
||||
Name: "John Ronald Reuel Tolkien",
|
||||
}
|
||||
author, err := s.mutationResolver.UpdateAuthor(s.AdminCtx, authorID, input)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(author)
|
||||
s.Equal("John Ronald Reuel Tolkien", author.Name)
|
||||
})
|
||||
|
||||
s.Run("Delete Author", func() {
|
||||
ok, err := s.mutationResolver.DeleteAuthor(s.AdminCtx, authorID)
|
||||
s.Require().NoError(err)
|
||||
s.True(ok)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthorResolversTestSuite) TestAuthorQueries() {
|
||||
user := s.createUser("author-reader", "author-reader@test.com", "password", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
// Create an author to query
|
||||
input := model.AuthorInput{
|
||||
Name: "George Orwell",
|
||||
}
|
||||
createdAuthor, err := s.mutationResolver.CreateAuthor(ctx, input)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Get Author by ID", func() {
|
||||
author, err := s.queryResolver.Author(ctx, createdAuthor.ID)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(author)
|
||||
s.Equal("George Orwell", author.Name)
|
||||
})
|
||||
|
||||
s.Run("List Authors", func() {
|
||||
authors, err := s.queryResolver.Authors(ctx, nil, nil, nil, nil)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(authors)
|
||||
s.True(len(authors) >= 1)
|
||||
})
|
||||
}
|
||||
199
internal/adapters/graphql/book_resolvers_test.go
Normal file
199
internal/adapters/graphql/book_resolvers_test.go
Normal file
@ -0,0 +1,199 @@
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type BookResolversTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
queryResolver graphql.QueryResolver
|
||||
mutationResolver graphql.MutationResolver
|
||||
}
|
||||
|
||||
func TestBookResolvers(t *testing.T) {
|
||||
suite.Run(t, new(BookResolversTestSuite))
|
||||
}
|
||||
|
||||
func (s *BookResolversTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(&testutil.TestConfig{
|
||||
DBPath: "book_resolvers_test.db",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BookResolversTestSuite) TearDownSuite() {
|
||||
s.IntegrationTestSuite.TearDownSuite()
|
||||
os.Remove("book_resolvers_test.db")
|
||||
}
|
||||
|
||||
func (s *BookResolversTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
resolver := &graphql.Resolver{App: s.App}
|
||||
s.queryResolver = resolver.Query()
|
||||
s.mutationResolver = resolver.Mutation()
|
||||
}
|
||||
|
||||
// Helper to create a user for tests
|
||||
func (s *BookResolversTestSuite) createUser(username, email, password string, role domain.UserRole) *domain.User {
|
||||
resp, err := s.App.Auth.Commands.Register(context.Background(), auth.RegisterInput{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
user, err := s.App.User.Queries.User(context.Background(), resp.User.ID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
if role != user.Role {
|
||||
user.Role = role
|
||||
err = s.DB.Save(user).Error
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// Helper to create a context with JWT claims
|
||||
func (s *BookResolversTestSuite) contextWithClaims(user *domain.User) context.Context {
|
||||
return testutil.ContextWithClaims(context.Background(), &platform_auth.Claims{
|
||||
UserID: user.ID,
|
||||
Role: string(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BookResolversTestSuite) TestCreateBook() {
|
||||
user := s.createUser("book-creator", "book-creator@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Arrange
|
||||
description := "A test book description."
|
||||
isbn := "978-0321765723"
|
||||
input := model.BookInput{
|
||||
Name: "My First Book",
|
||||
Language: "en",
|
||||
Description: &description,
|
||||
Isbn: &isbn,
|
||||
}
|
||||
|
||||
// Act
|
||||
book, err := s.mutationResolver.CreateBook(ctx, input)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(book)
|
||||
s.Equal("My First Book", book.Name)
|
||||
s.Equal("en", book.Language)
|
||||
s.Equal(description, *book.Description)
|
||||
s.Equal(isbn, *book.Isbn)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BookResolversTestSuite) TestUpdateBook() {
|
||||
user := s.createUser("book-updater", "book-updater@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
// Create a book to update
|
||||
description := "Initial description"
|
||||
isbn := "978-1491904244"
|
||||
createInput := model.BookInput{
|
||||
Name: "Updatable Book",
|
||||
Language: "en",
|
||||
Description: &description,
|
||||
Isbn: &isbn,
|
||||
}
|
||||
createdBook, err := s.mutationResolver.CreateBook(ctx, createInput)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Arrange
|
||||
updatedDescription := "Updated description"
|
||||
updateInput := model.BookInput{
|
||||
Name: "Updated Book Title",
|
||||
Language: "en",
|
||||
Description: &updatedDescription,
|
||||
Isbn: &isbn,
|
||||
}
|
||||
|
||||
// Act
|
||||
updatedBook, err := s.mutationResolver.UpdateBook(s.AdminCtx, createdBook.ID, updateInput)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(updatedBook)
|
||||
s.Equal("Updated Book Title", updatedBook.Name)
|
||||
s.Equal(updatedDescription, *updatedBook.Description)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BookResolversTestSuite) TestDeleteBook() {
|
||||
user := s.createUser("book-deletor", "book-deletor@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
// Create a book to delete
|
||||
description := "Deletable description"
|
||||
isbn := "978-1491904245"
|
||||
createInput := model.BookInput{
|
||||
Name: "Deletable Book",
|
||||
Language: "en",
|
||||
Description: &description,
|
||||
Isbn: &isbn,
|
||||
}
|
||||
createdBook, err := s.mutationResolver.CreateBook(ctx, createInput)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Act
|
||||
ok, err := s.mutationResolver.DeleteBook(s.AdminCtx, createdBook.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(ok)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BookResolversTestSuite) TestBookQueries() {
|
||||
user := s.createUser("book-reader", "book-reader@test.com", "password", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
// Create a book to query
|
||||
description := "Queryable description"
|
||||
isbn := "978-1491904246"
|
||||
createInput := model.BookInput{
|
||||
Name: "Queryable Book",
|
||||
Language: "en",
|
||||
Description: &description,
|
||||
Isbn: &isbn,
|
||||
}
|
||||
createdBook, err := s.mutationResolver.CreateBook(ctx, createInput)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Get Book by ID", func() {
|
||||
// Act
|
||||
book, err := s.queryResolver.Book(ctx, createdBook.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(book)
|
||||
s.Equal("Queryable Book", book.Name)
|
||||
})
|
||||
|
||||
s.Run("List Books", func() {
|
||||
// Act
|
||||
books, err := s.queryResolver.Books(ctx, nil, nil)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(books)
|
||||
s.True(len(books) >= 1)
|
||||
})
|
||||
}
|
||||
@ -126,7 +126,7 @@ type GetWorkResponse struct {
|
||||
// TestQueryWork tests the work query
|
||||
func (s *GraphQLIntegrationSuite) TestQueryWork() {
|
||||
// Create a test work with content
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content for work")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content for work")
|
||||
|
||||
// Define the query
|
||||
query := `
|
||||
@ -169,9 +169,9 @@ type GetWorksResponse struct {
|
||||
// TestQueryWorks tests the works query
|
||||
func (s *GraphQLIntegrationSuite) TestQueryWorks() {
|
||||
// Create test works
|
||||
s.CreateTestWork("Test Work 1", "en", "Test content for work 1")
|
||||
s.CreateTestWork("Test Work 2", "en", "Test content for work 2")
|
||||
s.CreateTestWork("Test Work 3", "fr", "Test content for work 3")
|
||||
s.CreateTestWork(s.AdminCtx, "Test Work 1", "en", "Test content for work 1")
|
||||
s.CreateTestWork(s.AdminCtx, "Test Work 2", "en", "Test content for work 2")
|
||||
s.CreateTestWork(s.AdminCtx, "Test Work 3", "fr", "Test content for work 3")
|
||||
|
||||
// Define the query
|
||||
query := `
|
||||
@ -250,8 +250,7 @@ func (s *GraphQLIntegrationSuite) TestCreateWork() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
response, err := executeGraphQL[CreateWorkResponse](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[CreateWorkResponse](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
|
||||
@ -356,8 +355,7 @@ func (s *GraphQLIntegrationSuite) TestCreateWorkValidation() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
|
||||
@ -368,7 +366,7 @@ func (s *GraphQLIntegrationSuite) TestCreateWorkValidation() {
|
||||
func (s *GraphQLIntegrationSuite) TestUpdateWorkValidation() {
|
||||
s.Run("should return error for invalid input", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Define the mutation
|
||||
mutation := `
|
||||
@ -389,8 +387,7 @@ func (s *GraphQLIntegrationSuite) TestUpdateWorkValidation() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
|
||||
@ -418,8 +415,7 @@ func (s *GraphQLIntegrationSuite) TestCreateAuthorValidation() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
|
||||
@ -452,8 +448,7 @@ func (s *GraphQLIntegrationSuite) TestUpdateAuthorValidation() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
|
||||
@ -464,7 +459,7 @@ func (s *GraphQLIntegrationSuite) TestUpdateAuthorValidation() {
|
||||
func (s *GraphQLIntegrationSuite) TestCreateTranslationValidation() {
|
||||
s.Run("should return error for invalid input", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Define the mutation
|
||||
mutation := `
|
||||
@ -485,8 +480,7 @@ func (s *GraphQLIntegrationSuite) TestCreateTranslationValidation() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
|
||||
@ -497,7 +491,7 @@ func (s *GraphQLIntegrationSuite) TestCreateTranslationValidation() {
|
||||
func (s *GraphQLIntegrationSuite) TestUpdateTranslationValidation() {
|
||||
s.Run("should return error for invalid input", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
createdTranslation, err := s.App.Translation.Commands.CreateOrUpdateTranslation(s.AdminCtx, translation.CreateOrUpdateTranslationInput{
|
||||
Title: "Test Translation",
|
||||
Language: "en",
|
||||
@ -527,8 +521,7 @@ func (s *GraphQLIntegrationSuite) TestUpdateTranslationValidation() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().NotNil(response.Errors, "GraphQL mutation should return errors")
|
||||
@ -539,8 +532,7 @@ func (s *GraphQLIntegrationSuite) TestUpdateTranslationValidation() {
|
||||
func (s *GraphQLIntegrationSuite) TestDeleteWork() {
|
||||
s.Run("should delete a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Define the mutation
|
||||
mutation := `
|
||||
@ -555,7 +547,7 @@ func (s *GraphQLIntegrationSuite) TestDeleteWork() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
|
||||
@ -573,7 +565,6 @@ func (s *GraphQLIntegrationSuite) TestDeleteAuthor() {
|
||||
// Arrange
|
||||
createdAuthor, err := s.App.Author.Commands.CreateAuthor(context.Background(), author.CreateAuthorInput{Name: "Test Author"})
|
||||
s.Require().NoError(err)
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
|
||||
// Define the mutation
|
||||
mutation := `
|
||||
@ -588,7 +579,7 @@ func (s *GraphQLIntegrationSuite) TestDeleteAuthor() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
|
||||
@ -604,7 +595,7 @@ func (s *GraphQLIntegrationSuite) TestDeleteAuthor() {
|
||||
func (s *GraphQLIntegrationSuite) TestDeleteTranslation() {
|
||||
s.Run("should delete a translation", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
createdTranslation, err := s.App.Translation.Commands.CreateOrUpdateTranslation(s.AdminCtx, translation.CreateOrUpdateTranslationInput{
|
||||
Title: "Test Translation",
|
||||
Language: "en",
|
||||
@ -613,7 +604,6 @@ func (s *GraphQLIntegrationSuite) TestDeleteTranslation() {
|
||||
TranslatableType: "works",
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
_, adminToken := s.CreateAuthenticatedUser("admin", "admin@test.com", domain.UserRoleAdmin)
|
||||
|
||||
// Define the mutation
|
||||
mutation := `
|
||||
@ -628,7 +618,7 @@ func (s *GraphQLIntegrationSuite) TestDeleteTranslation() {
|
||||
}
|
||||
|
||||
// Execute the mutation
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
|
||||
@ -649,7 +639,6 @@ func TestGraphQLIntegrationSuite(t *testing.T) {
|
||||
func (s *GraphQLIntegrationSuite) TestBookMutations() {
|
||||
// Create users for testing authorization
|
||||
_, readerToken := s.CreateAuthenticatedUser("bookreader", "bookreader@test.com", domain.UserRoleReader)
|
||||
_, adminToken := s.CreateAuthenticatedUser("bookadmin", "bookadmin@test.com", domain.UserRoleAdmin)
|
||||
|
||||
var bookID string
|
||||
|
||||
@ -738,7 +727,7 @@ func (s *GraphQLIntegrationSuite) TestBookMutations() {
|
||||
}
|
||||
|
||||
// Execute the mutation with the admin's token
|
||||
response, err := executeGraphQL[UpdateBookResponse](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[UpdateBookResponse](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(response)
|
||||
s.Require().Nil(response.Errors, "GraphQL mutation should not return errors")
|
||||
@ -777,7 +766,7 @@ func (s *GraphQLIntegrationSuite) TestBookMutations() {
|
||||
}
|
||||
|
||||
// Execute the mutation with the admin's token
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &adminToken)
|
||||
response, err := executeGraphQL[any](s, mutation, variables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(response.Errors)
|
||||
s.True(response.Data.(map[string]interface{})["deleteBook"].(bool))
|
||||
@ -786,7 +775,6 @@ func (s *GraphQLIntegrationSuite) TestBookMutations() {
|
||||
|
||||
func (s *GraphQLIntegrationSuite) TestBookQueries() {
|
||||
// Create a book to query
|
||||
_, adminToken := s.CreateAuthenticatedUser("bookadmin2", "bookadmin2@test.com", domain.UserRoleAdmin)
|
||||
createMutation := `
|
||||
mutation CreateBook($input: BookInput!) {
|
||||
createBook(input: $input) {
|
||||
@ -802,7 +790,7 @@ func (s *GraphQLIntegrationSuite) TestBookQueries() {
|
||||
"isbn": "978-0-306-40615-7",
|
||||
},
|
||||
}
|
||||
createResponse, err := executeGraphQL[CreateBookResponse](s, createMutation, createVariables, &adminToken)
|
||||
createResponse, err := executeGraphQL[CreateBookResponse](s, createMutation, createVariables, &s.AdminToken)
|
||||
s.Require().NoError(err)
|
||||
bookID := createResponse.Data.CreateBook.ID
|
||||
|
||||
@ -916,7 +904,7 @@ func (s *GraphQLIntegrationSuite) TestCommentMutations() {
|
||||
_ = otherUser
|
||||
|
||||
// Create a work to comment on
|
||||
work := s.CreateTestWork("Commentable Work", "en", "Some content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Commentable Work", "en", "Some content")
|
||||
|
||||
var commentID string
|
||||
|
||||
@ -1043,7 +1031,7 @@ func (s *GraphQLIntegrationSuite) TestLikeMutations() {
|
||||
_ = otherUser
|
||||
|
||||
// Create a work to like
|
||||
work := s.CreateTestWork("Likeable Work", "en", "Some content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Likeable Work", "en", "Some content")
|
||||
|
||||
var likeID string
|
||||
|
||||
@ -1132,7 +1120,7 @@ func (s *GraphQLIntegrationSuite) TestBookmarkMutations() {
|
||||
_ = otherUser
|
||||
|
||||
// Create a work to bookmark
|
||||
work := s.CreateTestWork("Bookmarkable Work", "en", "Some content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Bookmarkable Work", "en", "Some content")
|
||||
|
||||
s.Run("should create a bookmark on a work", func() {
|
||||
// Define the mutation
|
||||
@ -1239,8 +1227,8 @@ type TrendingWorksResponse struct {
|
||||
func (s *GraphQLIntegrationSuite) TestTrendingWorksQuery() {
|
||||
s.Run("should return a list of trending works", func() {
|
||||
// Arrange
|
||||
work1 := s.CreateTestWork("Work 1", "en", "content")
|
||||
work2 := s.CreateTestWork("Work 2", "en", "content")
|
||||
work1 := s.CreateTestWork(s.AdminCtx, "Work 1", "en", "content")
|
||||
work2 := s.CreateTestWork(s.AdminCtx, "Work 2", "en", "content")
|
||||
s.DB.Create(&domain.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
|
||||
s.DB.Create(&domain.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
|
||||
s.Require().NoError(s.App.Analytics.UpdateTrending(context.Background()))
|
||||
@ -1363,7 +1351,7 @@ func (s *GraphQLIntegrationSuite) TestCollectionMutations() {
|
||||
|
||||
s.Run("should add a work to a collection", func() {
|
||||
// Create a work
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Define the mutation
|
||||
mutation := `
|
||||
@ -1388,7 +1376,7 @@ func (s *GraphQLIntegrationSuite) TestCollectionMutations() {
|
||||
|
||||
s.Run("should remove a work from a collection", func() {
|
||||
// Create a work and add it to the collection first
|
||||
work := s.CreateTestWork("Another Work", "en", "Some content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Another Work", "en", "Some content")
|
||||
collectionIDInt, err := strconv.ParseUint(collectionID, 10, 64)
|
||||
s.Require().NoError(err)
|
||||
err = s.App.Collection.Commands.AddWorkToCollection(context.Background(), collection.AddWorkToCollectionInput{
|
||||
|
||||
@ -1262,11 +1262,11 @@ func (r *queryResolver) Work(ctx context.Context, id string) (*model.Work, error
|
||||
|
||||
workDTO, err := r.App.Work.Queries.GetWorkByID(ctx, uint(workID))
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if workDTO == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := r.App.Analytics.IncrementWorkViews(context.Background(), uint(workID)); err != nil {
|
||||
@ -1674,9 +1674,6 @@ func (r *queryResolver) UserProfile(ctx context.Context, userID string) (*model.
|
||||
|
||||
profile, err := r.App.User.Queries.UserProfile(ctx, uint(uID))
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if profile == nil {
|
||||
|
||||
197
internal/adapters/graphql/translation_resolvers_test.go
Normal file
197
internal/adapters/graphql/translation_resolvers_test.go
Normal file
@ -0,0 +1,197 @@
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type TranslationResolversTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
queryResolver graphql.QueryResolver
|
||||
mutationResolver graphql.MutationResolver
|
||||
}
|
||||
|
||||
func TestTranslationResolvers(t *testing.T) {
|
||||
suite.Run(t, new(TranslationResolversTestSuite))
|
||||
}
|
||||
|
||||
func (s *TranslationResolversTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(&testutil.TestConfig{
|
||||
DBPath: "translation_resolvers_test.db",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *TranslationResolversTestSuite) TearDownSuite() {
|
||||
s.IntegrationTestSuite.TearDownSuite()
|
||||
os.Remove("translation_resolvers_test.db")
|
||||
}
|
||||
|
||||
func (s *TranslationResolversTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
resolver := &graphql.Resolver{App: s.App}
|
||||
s.queryResolver = resolver.Query()
|
||||
s.mutationResolver = resolver.Mutation()
|
||||
}
|
||||
|
||||
// Helper to create a user for tests
|
||||
func (s *TranslationResolversTestSuite) createUser(username, email, password string, role domain.UserRole) *domain.User {
|
||||
resp, err := s.App.Auth.Commands.Register(context.Background(), auth.RegisterInput{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
user, err := s.App.User.Queries.User(context.Background(), resp.User.ID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
if role != user.Role {
|
||||
user.Role = role
|
||||
err = s.DB.Save(user).Error
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// Helper to create a context with JWT claims
|
||||
func (s *TranslationResolversTestSuite) contextWithClaims(user *domain.User) context.Context {
|
||||
return testutil.ContextWithClaims(context.Background(), &platform_auth.Claims{
|
||||
UserID: user.ID,
|
||||
Role: string(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *TranslationResolversTestSuite) TestCreateTranslation() {
|
||||
user := s.createUser("translator", "translator@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
work := s.CreateTestWork(ctx, "Test Work for Translation", "en", "Original Content")
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Arrange
|
||||
content := "Translated Content"
|
||||
input := model.TranslationInput{
|
||||
Name: "Spanish Translation",
|
||||
Language: "es",
|
||||
WorkID: fmt.Sprintf("%d", work.ID),
|
||||
Content: &content,
|
||||
}
|
||||
|
||||
// Act
|
||||
translation, err := s.mutationResolver.CreateTranslation(ctx, input)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(translation)
|
||||
s.Equal("Spanish Translation", translation.Name)
|
||||
s.Equal("es", translation.Language)
|
||||
s.Equal("Translated Content", *translation.Content)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *TranslationResolversTestSuite) TestUpdateTranslation() {
|
||||
user := s.createUser("translator-updater", "translator-updater@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
work := s.CreateTestWork(ctx, "Test Work for Translation Update", "en", "Original Content")
|
||||
|
||||
content := "Initial Translated Content"
|
||||
createInput := model.TranslationInput{
|
||||
Name: "Updatable Translation",
|
||||
Language: "fr",
|
||||
WorkID: fmt.Sprintf("%d", work.ID),
|
||||
Content: &content,
|
||||
}
|
||||
createdTranslation, err := s.mutationResolver.CreateTranslation(ctx, createInput)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Arrange
|
||||
updatedContent := "Updated French Content"
|
||||
updateInput := model.TranslationInput{
|
||||
Name: "Updated French Translation",
|
||||
Language: "fr",
|
||||
WorkID: fmt.Sprintf("%d", work.ID),
|
||||
Content: &updatedContent,
|
||||
}
|
||||
|
||||
// Act
|
||||
updatedTranslation, err := s.mutationResolver.UpdateTranslation(ctx, createdTranslation.ID, updateInput)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(updatedTranslation)
|
||||
s.Equal("Updated French Translation", updatedTranslation.Name)
|
||||
s.Equal("fr", updatedTranslation.Language)
|
||||
s.Equal("Updated French Content", *updatedTranslation.Content)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *TranslationResolversTestSuite) TestDeleteTranslation() {
|
||||
user := s.createUser("translator-deletor", "translator-deletor@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
work := s.CreateTestWork(ctx, "Test Work for Translation Deletion", "en", "Original Content")
|
||||
|
||||
content := "Content to be deleted"
|
||||
createInput := model.TranslationInput{
|
||||
Name: "Deletable Translation",
|
||||
Language: "de",
|
||||
WorkID: fmt.Sprintf("%d", work.ID),
|
||||
Content: &content,
|
||||
}
|
||||
createdTranslation, err := s.mutationResolver.CreateTranslation(ctx, createInput)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Act
|
||||
ok, err := s.mutationResolver.DeleteTranslation(ctx, createdTranslation.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(ok)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *TranslationResolversTestSuite) TestTranslationQueries() {
|
||||
user := s.createUser("translator-reader", "translator-reader@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
work := s.CreateTestWork(ctx, "Test Work for Translation Queries", "en", "Original Content")
|
||||
|
||||
content := "Queried Content"
|
||||
createInput := model.TranslationInput{
|
||||
Name: "Queried Translation",
|
||||
Language: "it",
|
||||
WorkID: fmt.Sprintf("%d", work.ID),
|
||||
Content: &content,
|
||||
}
|
||||
createdTranslation, err := s.mutationResolver.CreateTranslation(ctx, createInput)
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Get Translation by ID", func() {
|
||||
// Act
|
||||
translation, err := s.queryResolver.Translation(ctx, createdTranslation.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(translation)
|
||||
s.Equal("Queried Translation", translation.Name)
|
||||
})
|
||||
|
||||
s.Run("List Translations for a Work", func() {
|
||||
// Act
|
||||
translations, err := s.queryResolver.Translations(ctx, fmt.Sprintf("%d", work.ID), nil, nil, nil)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(translations)
|
||||
s.Len(translations, 2) // Original + Italian
|
||||
})
|
||||
}
|
||||
637
internal/adapters/graphql/user_resolvers_unit_test.go
Normal file
637
internal/adapters/graphql/user_resolvers_unit_test.go
Normal file
@ -0,0 +1,637 @@
|
||||
package graphql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"tercul/internal/app"
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/app/user"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// mockUserRepositoryForUserResolver is a mock for the user repository.
|
||||
type mockUserRepositoryForUserResolver struct{ mock.Mock }
|
||||
|
||||
// Implement domain.UserRepository
|
||||
func (m *mockUserRepositoryForUserResolver) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||
args := m.Called(ctx, username)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
args := m.Called(ctx, email)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||
args := m.Called(ctx, role)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) Create(ctx context.Context, entity *domain.User) error {
|
||||
return m.Called(ctx, entity).Error(0)
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) Update(ctx context.Context, entity *domain.User) error {
|
||||
return m.Called(ctx, entity).Error(0)
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) Delete(ctx context.Context, id uint) error {
|
||||
return m.Called(ctx, id).Error(0)
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) ListAll(ctx context.Context) ([]domain.User, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockUserRepositoryForUserResolver) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserRepositoryForUserResolver) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mockUserProfileRepository is a mock for the user profile repository.
|
||||
type mockUserProfileRepository struct{ mock.Mock }
|
||||
|
||||
// Implement domain.UserProfileRepository
|
||||
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) {
|
||||
args := m.Called(ctx, userID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.UserProfile), args.Error(1)
|
||||
}
|
||||
|
||||
// Implement BaseRepository methods for UserProfile
|
||||
func (m *mockUserProfileRepository) Create(ctx context.Context, entity *domain.UserProfile) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*domain.UserProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.UserProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) Update(ctx context.Context, entity *domain.UserProfile) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||
func (m *mockUserProfileRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.UserProfile], error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.UserProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) ListAll(ctx context.Context) ([]domain.UserProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockUserProfileRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.UserProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.UserProfile, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (m *mockUserProfileRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockUserProfileRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserResolversUnitSuite is a unit test suite for the user resolvers.
|
||||
type UserResolversUnitSuite struct {
|
||||
suite.Suite
|
||||
resolver *Resolver
|
||||
mockUserRepo *mockUserRepositoryForUserResolver
|
||||
mockUserProfileRepo *mockUserProfileRepository
|
||||
}
|
||||
|
||||
// SetupTest sets up the test suite
|
||||
func (s *UserResolversUnitSuite) SetupTest() {
|
||||
s.mockUserRepo = new(mockUserRepositoryForUserResolver)
|
||||
s.mockUserProfileRepo = new(mockUserProfileRepository)
|
||||
|
||||
// The authz service dependencies are not needed for the user commands being tested.
|
||||
authzSvc := authz.NewService(nil, nil, s.mockUserRepo, nil)
|
||||
|
||||
userCommands := user.NewUserCommands(s.mockUserRepo, authzSvc)
|
||||
userQueries := user.NewUserQueries(s.mockUserRepo, s.mockUserProfileRepo)
|
||||
|
||||
userService := &user.Service{
|
||||
Commands: userCommands,
|
||||
Queries: userQueries,
|
||||
}
|
||||
|
||||
s.resolver = &Resolver{
|
||||
App: &app.Application{
|
||||
User: userService,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserResolversUnitSuite runs the test suite
|
||||
func TestUserResolversUnitSuite(t *testing.T) {
|
||||
suite.Run(t, new(UserResolversUnitSuite))
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestUserQuery() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
userID := uint(1)
|
||||
userIDStr := "1"
|
||||
ctx := context.Background()
|
||||
|
||||
expectedUser := &domain.User{
|
||||
Username: "testuser",
|
||||
Email: "test@test.com",
|
||||
Role: domain.UserRoleReader,
|
||||
}
|
||||
expectedUser.ID = userID
|
||||
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(expectedUser, nil).Once()
|
||||
|
||||
gqlUser, err := s.resolver.Query().User(ctx, userIDStr)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(gqlUser)
|
||||
s.Equal(userIDStr, gqlUser.ID)
|
||||
s.Equal(expectedUser.Username, gqlUser.Username)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Not Found", func() {
|
||||
s.SetupTest()
|
||||
userID := uint(99)
|
||||
userIDStr := "99"
|
||||
ctx := context.Background()
|
||||
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(nil, domain.ErrEntityNotFound).Once()
|
||||
|
||||
gqlUser, err := s.resolver.Query().User(ctx, userIDStr)
|
||||
|
||||
s.Require().Error(err) // The resolver should propagate the error
|
||||
s.Require().Nil(gqlUser)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Invalid ID", func() {
|
||||
s.SetupTest()
|
||||
ctx := context.Background()
|
||||
_, err := s.resolver.Query().User(ctx, "invalid")
|
||||
s.Require().Error(err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestUserProfileQuery() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
userID := uint(1)
|
||||
userIDStr := "1"
|
||||
ctx := context.Background()
|
||||
|
||||
expectedProfile := &domain.UserProfile{
|
||||
UserID: userID,
|
||||
PhoneNumber: "12345",
|
||||
}
|
||||
expectedProfile.ID = 1
|
||||
|
||||
expectedUser := &domain.User{
|
||||
Username: "testuser",
|
||||
}
|
||||
expectedUser.ID = userID
|
||||
|
||||
s.mockUserProfileRepo.On("GetByUserID", mock.Anything, userID).Return(expectedProfile, nil).Once()
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(expectedUser, nil).Once()
|
||||
|
||||
gqlProfile, err := s.resolver.Query().UserProfile(ctx, userIDStr)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(gqlProfile)
|
||||
s.Equal(userIDStr, gqlProfile.UserID)
|
||||
s.Equal(&expectedProfile.PhoneNumber, gqlProfile.PhoneNumber)
|
||||
s.mockUserProfileRepo.AssertExpectations(s.T())
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Profile Not Found", func() {
|
||||
s.SetupTest()
|
||||
userID := uint(99)
|
||||
userIDStr := "99"
|
||||
ctx := context.Background()
|
||||
|
||||
s.mockUserProfileRepo.On("GetByUserID", mock.Anything, userID).Return(nil, domain.ErrEntityNotFound).Once()
|
||||
|
||||
gqlProfile, err := s.resolver.Query().UserProfile(ctx, userIDStr)
|
||||
|
||||
s.Require().Error(err)
|
||||
s.Require().Nil(gqlProfile)
|
||||
s.mockUserProfileRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("User Not Found for profile", func() {
|
||||
s.SetupTest()
|
||||
userID := uint(1)
|
||||
userIDStr := "1"
|
||||
ctx := context.Background()
|
||||
|
||||
expectedProfile := &domain.UserProfile{
|
||||
UserID: userID,
|
||||
}
|
||||
expectedProfile.ID = 1
|
||||
|
||||
s.mockUserProfileRepo.On("GetByUserID", mock.Anything, userID).Return(expectedProfile, nil).Once()
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(nil, domain.ErrEntityNotFound).Once()
|
||||
|
||||
_, err := s.resolver.Query().UserProfile(ctx, userIDStr)
|
||||
|
||||
s.Require().Error(err)
|
||||
s.mockUserProfileRepo.AssertExpectations(s.T())
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestUpdateProfileMutation() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
actorID := uint(1)
|
||||
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: actorID,
|
||||
Role: string(domain.UserRoleReader),
|
||||
})
|
||||
|
||||
displayName := "New Name"
|
||||
input := model.UserInput{DisplayName: &displayName}
|
||||
|
||||
originalUser := &domain.User{DisplayName: "Old Name"}
|
||||
originalUser.ID = actorID
|
||||
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, actorID).Return(originalUser, nil).Once()
|
||||
s.mockUserRepo.On("Update", mock.Anything, mock.MatchedBy(func(u *domain.User) bool {
|
||||
return u.ID == actorID && u.DisplayName == displayName
|
||||
})).Return(nil).Once()
|
||||
|
||||
updatedUser, err := s.resolver.Mutation().UpdateProfile(ctx, input)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(updatedUser)
|
||||
s.Equal(displayName, *updatedUser.DisplayName)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Unauthorized", func() {
|
||||
s.SetupTest()
|
||||
ctx := context.Background() // no user
|
||||
displayName := "New Name"
|
||||
input := model.UserInput{DisplayName: &displayName}
|
||||
|
||||
_, err := s.resolver.Mutation().UpdateProfile(ctx, input)
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrUnauthorized)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestUpdateUserMutation() {
|
||||
s.Run("Success as self", func() {
|
||||
s.SetupTest()
|
||||
actorID := uint(1)
|
||||
targetID := uint(1)
|
||||
targetIDStr := "1"
|
||||
username := "new_username"
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: actorID,
|
||||
Role: string(domain.UserRoleReader),
|
||||
})
|
||||
input := model.UserInput{Username: &username}
|
||||
|
||||
originalUser := &domain.User{Username: "old_username"}
|
||||
originalUser.ID = targetID
|
||||
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, targetID).Return(originalUser, nil).Once()
|
||||
s.mockUserRepo.On("Update", mock.Anything, mock.MatchedBy(func(u *domain.User) bool {
|
||||
return u.ID == targetID && u.Username == username
|
||||
})).Return(nil).Once()
|
||||
|
||||
updatedUser, err := s.resolver.Mutation().UpdateUser(ctx, targetIDStr, input)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(updatedUser)
|
||||
s.Equal(username, updatedUser.Username)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Success as admin", func() {
|
||||
s.SetupTest()
|
||||
actorID := uint(99) // Admin
|
||||
targetID := uint(1)
|
||||
targetIDStr := "1"
|
||||
username := "new_username"
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: actorID,
|
||||
Role: string(domain.UserRoleAdmin),
|
||||
})
|
||||
input := model.UserInput{Username: &username}
|
||||
|
||||
originalUser := &domain.User{Username: "old_username"}
|
||||
originalUser.ID = targetID
|
||||
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, targetID).Return(originalUser, nil).Once()
|
||||
s.mockUserRepo.On("Update", mock.Anything, mock.MatchedBy(func(u *domain.User) bool {
|
||||
return u.ID == targetID && u.Username == username
|
||||
})).Return(nil).Once()
|
||||
|
||||
updatedUser, err := s.resolver.Mutation().UpdateUser(ctx, targetIDStr, input)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(updatedUser)
|
||||
s.Equal(username, updatedUser.Username)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Forbidden", func() {
|
||||
s.SetupTest()
|
||||
actorID := uint(2)
|
||||
targetIDStr := "1"
|
||||
username := "new_username"
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: actorID,
|
||||
Role: string(domain.UserRoleReader),
|
||||
})
|
||||
input := model.UserInput{Username: &username}
|
||||
|
||||
_, err := s.resolver.Mutation().UpdateUser(ctx, targetIDStr, input)
|
||||
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrForbidden)
|
||||
s.mockUserRepo.AssertNotCalled(s.T(), "GetByID")
|
||||
s.mockUserRepo.AssertNotCalled(s.T(), "Update")
|
||||
})
|
||||
|
||||
s.Run("User not found", func() {
|
||||
s.SetupTest()
|
||||
actorID := uint(1)
|
||||
targetID := uint(1)
|
||||
targetIDStr := "1"
|
||||
username := "new_username"
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: actorID,
|
||||
Role: string(domain.UserRoleReader),
|
||||
})
|
||||
input := model.UserInput{Username: &username}
|
||||
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, targetID).Return(nil, domain.ErrEntityNotFound).Once()
|
||||
|
||||
_, err := s.resolver.Mutation().UpdateUser(ctx, targetIDStr, input)
|
||||
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrEntityNotFound)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
s.mockUserRepo.AssertNotCalled(s.T(), "Update")
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestUsersQuery() {
|
||||
s.Run("Success without role", func() {
|
||||
s.SetupTest()
|
||||
ctx := context.Background()
|
||||
expectedUsers := []domain.User{
|
||||
{Username: "user1"},
|
||||
{Username: "user2"},
|
||||
}
|
||||
s.mockUserRepo.On("ListAll", mock.Anything).Return(expectedUsers, nil).Once()
|
||||
|
||||
users, err := s.resolver.Query().Users(ctx, nil, nil, nil)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Len(users, 2)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Success with role", func() {
|
||||
s.SetupTest()
|
||||
ctx := context.Background()
|
||||
role := domain.UserRoleAdmin
|
||||
modelRole := model.UserRoleAdmin
|
||||
expectedUsers := []domain.User{
|
||||
{Username: "admin1", Role: role},
|
||||
}
|
||||
s.mockUserRepo.On("ListByRole", mock.Anything, role).Return(expectedUsers, nil).Once()
|
||||
|
||||
users, err := s.resolver.Query().Users(ctx, nil, nil, &modelRole)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Len(users, 1)
|
||||
s.Equal(model.UserRoleAdmin, users[0].Role)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestUserByEmailQuery() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
email := "test@test.com"
|
||||
ctx := context.Background()
|
||||
|
||||
expectedUser := &domain.User{
|
||||
Username: "testuser",
|
||||
Email: email,
|
||||
Role: domain.UserRoleReader,
|
||||
}
|
||||
expectedUser.ID = 1
|
||||
|
||||
s.mockUserRepo.On("FindByEmail", mock.Anything, email).Return(expectedUser, nil).Once()
|
||||
|
||||
gqlUser, err := s.resolver.Query().UserByEmail(ctx, email)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(gqlUser)
|
||||
s.Equal(email, gqlUser.Email)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestUserByUsernameQuery() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
username := "testuser"
|
||||
ctx := context.Background()
|
||||
|
||||
expectedUser := &domain.User{
|
||||
Username: username,
|
||||
Email: "test@test.com",
|
||||
Role: domain.UserRoleReader,
|
||||
}
|
||||
expectedUser.ID = 1
|
||||
|
||||
s.mockUserRepo.On("FindByUsername", mock.Anything, username).Return(expectedUser, nil).Once()
|
||||
|
||||
gqlUser, err := s.resolver.Query().UserByUsername(ctx, username)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(gqlUser)
|
||||
s.Equal(username, gqlUser.Username)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestMeQuery() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
userID := uint(1)
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), userID)
|
||||
|
||||
expectedUser := &domain.User{
|
||||
Username: "testuser",
|
||||
Email: "test@test.com",
|
||||
Role: domain.UserRoleReader,
|
||||
}
|
||||
expectedUser.ID = userID
|
||||
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(expectedUser, nil).Once()
|
||||
|
||||
gqlUser, err := s.resolver.Query().Me(ctx)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(gqlUser)
|
||||
s.Equal(fmt.Sprintf("%d", userID), gqlUser.ID)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Unauthorized", func() {
|
||||
s.SetupTest()
|
||||
ctx := context.Background() // No user in context
|
||||
_, err := s.resolver.Query().Me(ctx)
|
||||
s.Require().Error(err)
|
||||
s.Equal(domain.ErrUnauthorized, err)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserResolversUnitSuite) TestDeleteUserMutation() {
|
||||
s.Run("Success as self", func() {
|
||||
s.SetupTest()
|
||||
actorID := uint(1)
|
||||
targetID := uint(1)
|
||||
targetIDStr := "1"
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: actorID,
|
||||
Role: string(domain.UserRoleReader),
|
||||
})
|
||||
|
||||
s.mockUserRepo.On("Delete", mock.Anything, targetID).Return(nil).Once()
|
||||
|
||||
ok, err := s.resolver.Mutation().DeleteUser(ctx, targetIDStr)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.True(ok)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Success as admin", func() {
|
||||
s.SetupTest()
|
||||
actorID := uint(99) // Admin
|
||||
targetID := uint(1)
|
||||
targetIDStr := "1"
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: actorID,
|
||||
Role: string(domain.UserRoleAdmin),
|
||||
})
|
||||
|
||||
s.mockUserRepo.On("Delete", mock.Anything, targetID).Return(nil).Once()
|
||||
|
||||
ok, err := s.resolver.Mutation().DeleteUser(ctx, targetIDStr)
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.True(ok)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Forbidden", func() {
|
||||
s.SetupTest()
|
||||
actorID := uint(2)
|
||||
targetIDStr := "1"
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: actorID,
|
||||
Role: string(domain.UserRoleReader),
|
||||
})
|
||||
|
||||
ok, err := s.resolver.Mutation().DeleteUser(ctx, targetIDStr)
|
||||
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrForbidden)
|
||||
s.False(ok)
|
||||
s.mockUserRepo.AssertNotCalled(s.T(), "Delete")
|
||||
})
|
||||
|
||||
s.Run("Invalid ID", func() {
|
||||
s.SetupTest()
|
||||
ctx := context.Background()
|
||||
_, err := s.resolver.Mutation().DeleteUser(ctx, "invalid")
|
||||
s.Require().Error(err)
|
||||
})
|
||||
}
|
||||
260
internal/adapters/graphql/work_resolvers_test.go
Normal file
260
internal/adapters/graphql/work_resolvers_test.go
Normal file
@ -0,0 +1,260 @@
|
||||
package graphql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app/auth"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type WorkResolversTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
queryResolver graphql.QueryResolver
|
||||
mutationResolver graphql.MutationResolver
|
||||
}
|
||||
|
||||
func TestWorkResolvers(t *testing.T) {
|
||||
suite.Run(t, new(WorkResolversTestSuite))
|
||||
}
|
||||
|
||||
func (s *WorkResolversTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(&testutil.TestConfig{
|
||||
DBPath: "work_resolvers_test.db",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversTestSuite) TearDownSuite() {
|
||||
s.IntegrationTestSuite.TearDownSuite()
|
||||
os.Remove("work_resolvers_test.db")
|
||||
}
|
||||
|
||||
func (s *WorkResolversTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
resolver := &graphql.Resolver{App: s.App}
|
||||
s.queryResolver = resolver.Query()
|
||||
s.mutationResolver = resolver.Mutation()
|
||||
}
|
||||
|
||||
// Helper to create a user for tests
|
||||
func (s *WorkResolversTestSuite) createUser(username, email, password string, role domain.UserRole) *domain.User {
|
||||
resp, err := s.App.Auth.Commands.Register(context.Background(), auth.RegisterInput{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Password: password,
|
||||
})
|
||||
s.Require().NoError(err)
|
||||
|
||||
user, err := s.App.User.Queries.User(context.Background(), resp.User.ID)
|
||||
s.Require().NoError(err)
|
||||
|
||||
if role != user.Role {
|
||||
user.Role = role
|
||||
err = s.DB.Save(user).Error
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
// Helper to create a context with JWT claims
|
||||
func (s *WorkResolversTestSuite) contextWithClaims(user *domain.User) context.Context {
|
||||
return testutil.ContextWithClaims(context.Background(), &platform_auth.Claims{
|
||||
UserID: user.ID,
|
||||
Role: string(user.Role),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversTestSuite) TestCreateWork() {
|
||||
user := s.createUser("work-creator", "work-creator@test.com", "password", domain.UserRoleContributor)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Arrange
|
||||
input := model.WorkInput{
|
||||
Name: "My First Work",
|
||||
Language: "en",
|
||||
}
|
||||
|
||||
// Act
|
||||
work, err := s.mutationResolver.CreateWork(ctx, input)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(work)
|
||||
s.Equal("My First Work", work.Name)
|
||||
s.Equal("en", work.Language)
|
||||
|
||||
// Verify in DB
|
||||
dbWork, err := s.App.Work.Queries.GetWorkByID(context.Background(), 1)
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(dbWork)
|
||||
s.Equal("My First Work", dbWork.Title)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversTestSuite) TestWorkQuery() {
|
||||
// Arrange
|
||||
user := s.createUser("work-reader", "work-reader@test.com", "password", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
// Create a work to query
|
||||
domainWork := &domain.Work{Title: "Query Me", TranslatableModel: domain.TranslatableModel{Language: "es"}}
|
||||
createdWork, err := s.App.Work.Commands.CreateWork(ctx, domainWork)
|
||||
s.Require().NoError(err)
|
||||
workID := fmt.Sprintf("%d", createdWork.ID)
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Act
|
||||
work, err := s.queryResolver.Work(ctx, workID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(work)
|
||||
s.Equal("Query Me", work.Name)
|
||||
s.Equal("es", work.Language)
|
||||
})
|
||||
|
||||
s.Run("Not Found", func() {
|
||||
// Act
|
||||
work, err := s.queryResolver.Work(ctx, "99999")
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(work)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversTestSuite) TestUpdateWork() {
|
||||
// Arrange
|
||||
user := s.createUser("work-updater", "work-updater@test.com", "password", domain.UserRoleContributor)
|
||||
admin := s.createUser("work-admin", "work-admin@test.com", "password", domain.UserRoleAdmin)
|
||||
otherUser := s.createUser("other-user", "other-user@test.com", "password", domain.UserRoleContributor)
|
||||
|
||||
// Create a work to update
|
||||
domainWork := &domain.Work{Title: "Update Me", TranslatableModel: domain.TranslatableModel{Language: "fr"}}
|
||||
createdWork, err := s.App.Work.Commands.CreateWork(s.contextWithClaims(user), domainWork)
|
||||
s.Require().NoError(err)
|
||||
workID := fmt.Sprintf("%d", createdWork.ID)
|
||||
|
||||
s.Run("Success as owner", func() {
|
||||
// Arrange
|
||||
ctx := s.contextWithClaims(user)
|
||||
input := model.WorkInput{Name: "Updated Title", Language: "fr"}
|
||||
|
||||
// Act
|
||||
work, err := s.mutationResolver.UpdateWork(ctx, workID, input)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Equal("Updated Title", work.Name)
|
||||
})
|
||||
|
||||
s.Run("Success as admin", func() {
|
||||
// Arrange
|
||||
ctx := s.contextWithClaims(admin)
|
||||
input := model.WorkInput{Name: "Updated by Admin", Language: "fr"}
|
||||
|
||||
// Act
|
||||
work, err := s.mutationResolver.UpdateWork(ctx, workID, input)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Equal("Updated by Admin", work.Name)
|
||||
})
|
||||
|
||||
s.Run("Forbidden for other user", func() {
|
||||
// Arrange
|
||||
ctx := s.contextWithClaims(otherUser)
|
||||
input := model.WorkInput{Name: "Should Not Update", Language: "fr"}
|
||||
|
||||
// Act
|
||||
_, err := s.mutationResolver.UpdateWork(ctx, workID, input)
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversTestSuite) TestDeleteWork() {
|
||||
// Arrange
|
||||
user := s.createUser("work-deletor", "work-deletor@test.com", "password", domain.UserRoleContributor)
|
||||
admin := s.createUser("work-admin-deletor", "work-admin-deletor@test.com", "password", domain.UserRoleAdmin)
|
||||
otherUser := s.createUser("other-user-deletor", "other-user-deletor@test.com", "password", domain.UserRoleContributor)
|
||||
|
||||
s.Run("Success as owner", func() {
|
||||
// Arrange
|
||||
domainWork := &domain.Work{Title: "Delete Me", TranslatableModel: domain.TranslatableModel{Language: "de"}}
|
||||
createdWork, err := s.App.Work.Commands.CreateWork(s.contextWithClaims(user), domainWork)
|
||||
s.Require().NoError(err)
|
||||
workID := fmt.Sprintf("%d", createdWork.ID)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
// Act
|
||||
ok, err := s.mutationResolver.DeleteWork(ctx, workID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(ok)
|
||||
})
|
||||
|
||||
s.Run("Success as admin", func() {
|
||||
// Arrange
|
||||
domainWork := &domain.Work{Title: "Delete Me Admin", TranslatableModel: domain.TranslatableModel{Language: "de"}}
|
||||
createdWork, err := s.App.Work.Commands.CreateWork(s.contextWithClaims(user), domainWork)
|
||||
s.Require().NoError(err)
|
||||
workID := fmt.Sprintf("%d", createdWork.ID)
|
||||
ctx := s.contextWithClaims(admin)
|
||||
|
||||
// Act
|
||||
ok, err := s.mutationResolver.DeleteWork(ctx, workID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(ok)
|
||||
})
|
||||
|
||||
s.Run("Forbidden for other user", func() {
|
||||
// Arrange
|
||||
domainWork := &domain.Work{Title: "Don't Delete Me", TranslatableModel: domain.TranslatableModel{Language: "de"}}
|
||||
createdWork, err := s.App.Work.Commands.CreateWork(s.contextWithClaims(user), domainWork)
|
||||
s.Require().NoError(err)
|
||||
workID := fmt.Sprintf("%d", createdWork.ID)
|
||||
ctx := s.contextWithClaims(otherUser)
|
||||
|
||||
// Act
|
||||
_, err = s.mutationResolver.DeleteWork(ctx, workID)
|
||||
|
||||
// Assert
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversTestSuite) TestWorksQuery() {
|
||||
// Arrange
|
||||
user := s.createUser("works-reader", "works-reader@test.com", "password", domain.UserRoleReader)
|
||||
ctx := s.contextWithClaims(user)
|
||||
|
||||
// Create some works
|
||||
_, err := s.App.Work.Commands.CreateWork(ctx, &domain.Work{Title: "Work 1", TranslatableModel: domain.TranslatableModel{Language: "en"}})
|
||||
s.Require().NoError(err)
|
||||
_, err = s.App.Work.Commands.CreateWork(ctx, &domain.Work{Title: "Work 2", TranslatableModel: domain.TranslatableModel{Language: "en"}})
|
||||
s.Require().NoError(err)
|
||||
|
||||
s.Run("Success", func() {
|
||||
// Act
|
||||
works, err := s.queryResolver.Works(ctx, nil, nil, nil, nil, nil, nil, nil)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(len(works) >= 2) // >= because other tests might have created works
|
||||
})
|
||||
}
|
||||
455
internal/adapters/graphql/work_resolvers_unit_test.go
Normal file
455
internal/adapters/graphql/work_resolvers_unit_test.go
Normal file
@ -0,0 +1,455 @@
|
||||
package graphql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/adapters/graphql/model"
|
||||
"tercul/internal/app"
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/app/translation"
|
||||
"tercul/internal/app/work"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Mock Implementations
|
||||
type mockWorkRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockWorkRepository) Create(ctx context.Context, work *domain.Work) error {
|
||||
args := m.Called(ctx, work)
|
||||
work.ID = 1
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) IsAuthor(ctx context.Context, workID, authorID uint) (bool, error) {
|
||||
args := m.Called(ctx, workID, authorID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) Update(ctx context.Context, work *domain.Work) error {
|
||||
args := m.Called(ctx, work)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) Delete(ctx context.Context, id uint) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Work]), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) FindByTitle(ctx context.Context, title string) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) FindByLanguage(ctx context.Context, language string, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { return nil, nil }
|
||||
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) { return nil, nil }
|
||||
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return nil }
|
||||
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error { return nil }
|
||||
func (m *mockWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||
func (m *mockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||
func (m *mockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||
func (m *mockWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||
|
||||
|
||||
type mockAuthorRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockAuthorRepository) FindByName(ctx context.Context, name string) (*domain.Author, error) {
|
||||
args := m.Called(ctx, name)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) Create(ctx context.Context, author *domain.Author) error {
|
||||
args := m.Called(ctx, author)
|
||||
author.ID = 1
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { return nil }
|
||||
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) Update(ctx context.Context, entity *domain.Author) error { return nil }
|
||||
func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error { return nil }
|
||||
func (m *mockAuthorRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||
func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) { return 0, nil }
|
||||
func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) { return 0, nil }
|
||||
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) { return false, nil }
|
||||
func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) { return nil, nil }
|
||||
func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error { return nil }
|
||||
|
||||
type mockUserRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) { return nil, nil }
|
||||
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) { return nil, nil }
|
||||
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) { return nil, nil }
|
||||
func (m *mockUserRepository) Create(ctx context.Context, entity *domain.User) 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) Update(ctx context.Context, entity *domain.User) error { return nil }
|
||||
func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error { return nil }
|
||||
func (m *mockUserRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||
func (m *mockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error { return nil }
|
||||
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) { return nil, nil }
|
||||
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]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) 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 }
|
||||
|
||||
type mockSearchClient struct{ mock.Mock }
|
||||
|
||||
func (m *mockSearchClient) IndexWork(ctx context.Context, work *domain.Work, pipeline string) error {
|
||||
args := m.Called(ctx, work, pipeline)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockSearchClient) Search(ctx context.Context, query string, page, pageSize int, filters domain.SearchFilters) (*domain.SearchResults, error) {
|
||||
args := m.Called(ctx, query, page, pageSize, filters)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.SearchResults), args.Error(1)
|
||||
}
|
||||
|
||||
|
||||
type mockAnalyticsService struct{ mock.Mock }
|
||||
|
||||
func (m *mockAnalyticsService) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkViews(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) DecrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) { return nil, nil }
|
||||
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) { return nil, nil }
|
||||
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error { return nil }
|
||||
func (m *mockAnalyticsService) UpdateTrending(ctx context.Context) error { return nil }
|
||||
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) { return nil, nil }
|
||||
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error { return nil }
|
||||
|
||||
type mockTranslationRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockTranslationRepository) Upsert(ctx context.Context, translation *domain.Translation) error {
|
||||
args := m.Called(ctx, translation)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) { return nil, nil }
|
||||
func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) { return nil, 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) Create(ctx context.Context, entity *domain.Translation) error { return 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) Update(ctx context.Context, entity *domain.Translation) error { return nil }
|
||||
func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error { return nil }
|
||||
func (m *mockTranslationRepository) Delete(ctx context.Context, id uint) error { return nil }
|
||||
func (m *mockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, 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) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]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) 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 }
|
||||
|
||||
|
||||
// WorkResolversUnitSuite is a unit test suite for the work resolvers.
|
||||
type WorkResolversUnitSuite struct {
|
||||
suite.Suite
|
||||
resolver *Resolver
|
||||
mockWorkRepo *mockWorkRepository
|
||||
mockAuthorRepo *mockAuthorRepository
|
||||
mockUserRepo *mockUserRepository
|
||||
mockTranslationRepo *mockTranslationRepository
|
||||
mockSearchClient *mockSearchClient
|
||||
mockAnalyticsSvc *mockAnalyticsService
|
||||
}
|
||||
|
||||
func (s *WorkResolversUnitSuite) SetupTest() {
|
||||
s.mockWorkRepo = new(mockWorkRepository)
|
||||
s.mockAuthorRepo = new(mockAuthorRepository)
|
||||
s.mockUserRepo = new(mockUserRepository)
|
||||
s.mockTranslationRepo = new(mockTranslationRepository)
|
||||
s.mockSearchClient = new(mockSearchClient)
|
||||
s.mockAnalyticsSvc = new(mockAnalyticsService)
|
||||
|
||||
authzService := authz.NewService(s.mockWorkRepo, s.mockAuthorRepo, s.mockUserRepo, s.mockTranslationRepo)
|
||||
workCommands := work.NewWorkCommands(s.mockWorkRepo, s.mockAuthorRepo, s.mockUserRepo, s.mockSearchClient, authzService, s.mockAnalyticsSvc)
|
||||
workQueries := work.NewWorkQueries(s.mockWorkRepo)
|
||||
workService := work.NewService(s.mockWorkRepo, s.mockAuthorRepo, s.mockUserRepo, s.mockSearchClient, authzService, s.mockAnalyticsSvc)
|
||||
workService.Commands = workCommands
|
||||
workService.Queries = workQueries
|
||||
|
||||
translationCommands := translation.NewTranslationCommands(s.mockTranslationRepo, authzService)
|
||||
translationService := translation.NewService(s.mockTranslationRepo, authzService)
|
||||
translationService.Commands = translationCommands
|
||||
|
||||
s.resolver = &Resolver{
|
||||
App: &app.Application{
|
||||
Work: workService,
|
||||
Analytics: s.mockAnalyticsSvc,
|
||||
Translation: translationService,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkResolversUnitSuite(t *testing.T) {
|
||||
suite.Run(t, new(WorkResolversUnitSuite))
|
||||
}
|
||||
|
||||
func (s *WorkResolversUnitSuite) TestCreateWork_Unit() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
// 1. Setup
|
||||
userID := uint(1)
|
||||
workID := uint(1)
|
||||
authorID := uint(1)
|
||||
username := "testuser"
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), userID)
|
||||
content := "Test Content"
|
||||
input := model.WorkInput{
|
||||
Name: "Test Work",
|
||||
Language: "en",
|
||||
Content: &content,
|
||||
}
|
||||
user := &domain.User{Username: username}
|
||||
user.ID = userID
|
||||
author := &domain.Author{Name: username}
|
||||
author.ID = authorID
|
||||
work := &domain.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}
|
||||
|
||||
// 2. Mocking - Order is important here!
|
||||
// --- CreateWork Command ---
|
||||
// Get user to find author
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(user, nil).Once()
|
||||
// Find author by name (fails first time)
|
||||
s.mockAuthorRepo.On("FindByName", mock.Anything, username).Return(nil, domain.ErrEntityNotFound).Once()
|
||||
// Create author
|
||||
s.mockAuthorRepo.On("Create", mock.Anything, mock.AnythingOfType("*domain.Author")).Return(nil).Once()
|
||||
// Create work
|
||||
s.mockWorkRepo.On("Create", mock.Anything, mock.AnythingOfType("*domain.Work")).Return(nil).Once()
|
||||
// Index work
|
||||
s.mockSearchClient.On("IndexWork", mock.Anything, mock.Anything, "").Return(nil).Once()
|
||||
|
||||
// --- CreateOrUpdateTranslation Command (called from resolver) ---
|
||||
// Auth check: Get work by ID
|
||||
s.mockWorkRepo.On("GetByID", mock.Anything, workID).Return(work, nil).Once()
|
||||
// Auth check: Get user by ID
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(user, nil).Once()
|
||||
// Auth check: Find author by name (succeeds this time)
|
||||
s.mockAuthorRepo.On("FindByName", mock.Anything, username).Return(author, nil).Once()
|
||||
// Auth check: Check if user is author of the work
|
||||
s.mockWorkRepo.On("IsAuthor", mock.Anything, workID, authorID).Return(true, nil).Once()
|
||||
// Upsert translation
|
||||
s.mockTranslationRepo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.Translation")).Return(nil).Once()
|
||||
|
||||
// 3. Execution
|
||||
createdWork, err := s.resolver.Mutation().CreateWork(ctx, input)
|
||||
|
||||
// 4. Assertions
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(createdWork)
|
||||
s.Equal("Test Work", createdWork.Name)
|
||||
|
||||
// 5. Verify mock calls
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
s.mockAuthorRepo.AssertExpectations(s.T())
|
||||
s.mockWorkRepo.AssertExpectations(s.T())
|
||||
s.mockSearchClient.AssertExpectations(s.T())
|
||||
s.mockTranslationRepo.AssertExpectations(s.T())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversUnitSuite) TestUpdateWork_Unit() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
userID := uint(1)
|
||||
workID := uint(1)
|
||||
workIDStr := "1"
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), userID)
|
||||
input := model.WorkInput{Name: "Updated Work", Language: "en"}
|
||||
author := &domain.Author{}
|
||||
author.ID = 1
|
||||
|
||||
// Arrange
|
||||
s.mockWorkRepo.On("GetByID", mock.Anything, workID).Return(&domain.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}, nil).Once()
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(&domain.User{Username: "testuser"}, nil).Once()
|
||||
s.mockAuthorRepo.On("FindByName", mock.Anything, "testuser").Return(author, nil).Once()
|
||||
s.mockWorkRepo.On("IsAuthor", mock.Anything, workID, uint(1)).Return(true, nil).Once()
|
||||
s.mockWorkRepo.On("Update", mock.Anything, mock.AnythingOfType("*domain.Work")).Return(nil).Once()
|
||||
s.mockSearchClient.On("IndexWork", mock.Anything, mock.Anything, "").Return(nil).Once()
|
||||
|
||||
// Act
|
||||
_, err := s.resolver.Mutation().UpdateWork(ctx, workIDStr, input)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.mockWorkRepo.AssertExpectations(s.T())
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
s.mockAuthorRepo.AssertExpectations(s.T())
|
||||
s.mockSearchClient.AssertExpectations(s.T())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversUnitSuite) TestDeleteWork_Unit() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
userID := uint(1)
|
||||
workID := uint(1)
|
||||
workIDStr := "1"
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), userID)
|
||||
author := &domain.Author{}
|
||||
author.ID = 1
|
||||
|
||||
// Arrange
|
||||
s.mockWorkRepo.On("GetByID", mock.Anything, workID).Return(&domain.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}}}, nil).Once()
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, userID).Return(&domain.User{Username: "testuser"}, nil).Once()
|
||||
s.mockAuthorRepo.On("FindByName", mock.Anything, "testuser").Return(author, nil).Once()
|
||||
s.mockWorkRepo.On("IsAuthor", mock.Anything, workID, uint(1)).Return(true, nil).Once()
|
||||
s.mockWorkRepo.On("Delete", mock.Anything, workID).Return(nil).Once()
|
||||
|
||||
// Act
|
||||
ok, err := s.resolver.Mutation().DeleteWork(ctx, workIDStr)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.True(ok)
|
||||
s.mockWorkRepo.AssertExpectations(s.T())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversUnitSuite) TestWorkQuery_Unit() {
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
workID := uint(1)
|
||||
workIDStr := "1"
|
||||
ctx := context.Background()
|
||||
|
||||
// Arrange
|
||||
s.mockWorkRepo.On("GetByID", mock.Anything, workID).Return(&domain.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: workID}, Language: "en"}, Title: "Test Work"}, nil).Once()
|
||||
s.mockAnalyticsSvc.On("IncrementWorkViews", mock.Anything, workID).Return(nil).Once()
|
||||
s.mockWorkRepo.On("GetWithTranslations", mock.Anything, workID).Return(&domain.Work{
|
||||
Translations: []*domain.Translation{{Language: "en", Content: "Test Content"}},
|
||||
}, nil)
|
||||
|
||||
// Act
|
||||
work, err := s.resolver.Query().Work(ctx, workIDStr)
|
||||
time.Sleep(200 * time.Millisecond) // Allow time for goroutine to execute
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(work)
|
||||
s.Equal("Test Work", work.Name)
|
||||
s.Equal("Test Content", *work.Content)
|
||||
s.mockWorkRepo.AssertExpectations(s.T())
|
||||
s.mockAnalyticsSvc.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("Not Found", func() {
|
||||
s.SetupTest()
|
||||
workID := uint(1)
|
||||
workIDStr := "1"
|
||||
ctx := context.Background()
|
||||
s.mockWorkRepo.On("GetByID", mock.Anything, workID).Return(nil, domain.ErrEntityNotFound).Once()
|
||||
|
||||
// Act
|
||||
work, err := s.resolver.Query().Work(ctx, workIDStr)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().Nil(work)
|
||||
s.mockWorkRepo.AssertExpectations(s.T())
|
||||
})
|
||||
}
|
||||
|
||||
func (s *WorkResolversUnitSuite) TestWorksQuery_Unit() {
|
||||
ctx := context.Background()
|
||||
s.Run("Success", func() {
|
||||
s.SetupTest()
|
||||
limit := int32(10)
|
||||
offset := int32(0)
|
||||
s.mockWorkRepo.On("List", mock.Anything, 1, 10).Return(&domain.PaginatedResult[domain.Work]{
|
||||
Items: []domain.Work{
|
||||
{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}, Language: "en"}, Title: "Work 1"},
|
||||
},
|
||||
}, nil)
|
||||
s.mockWorkRepo.On("GetWithTranslations", mock.Anything, uint(1)).Return(&domain.Work{
|
||||
Translations: []*domain.Translation{{Language: "en", Content: "Content 1"}},
|
||||
}, nil)
|
||||
|
||||
_, err := s.resolver.Query().Works(ctx, &limit, &offset, nil, nil, nil, nil, nil)
|
||||
s.Require().NoError(err)
|
||||
})
|
||||
}
|
||||
@ -49,7 +49,7 @@ func (s *AnalyticsServiceTestSuite) SetupTest() {
|
||||
func (s *AnalyticsServiceTestSuite) TestIncrementWorkViews() {
|
||||
s.Run("should increment the view count for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Act
|
||||
err := s.service.IncrementWorkViews(context.Background(), work.ID)
|
||||
@ -65,7 +65,7 @@ func (s *AnalyticsServiceTestSuite) TestIncrementWorkViews() {
|
||||
func (s *AnalyticsServiceTestSuite) TestIncrementWorkLikes() {
|
||||
s.Run("should increment the like count for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Act
|
||||
err := s.service.IncrementWorkLikes(context.Background(), work.ID)
|
||||
@ -81,7 +81,7 @@ func (s *AnalyticsServiceTestSuite) TestIncrementWorkLikes() {
|
||||
func (s *AnalyticsServiceTestSuite) TestIncrementWorkComments() {
|
||||
s.Run("should increment the comment count for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Act
|
||||
err := s.service.IncrementWorkComments(context.Background(), work.ID)
|
||||
@ -97,7 +97,7 @@ func (s *AnalyticsServiceTestSuite) TestIncrementWorkComments() {
|
||||
func (s *AnalyticsServiceTestSuite) TestIncrementWorkBookmarks() {
|
||||
s.Run("should increment the bookmark count for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Act
|
||||
err := s.service.IncrementWorkBookmarks(context.Background(), work.ID)
|
||||
@ -113,7 +113,7 @@ func (s *AnalyticsServiceTestSuite) TestIncrementWorkBookmarks() {
|
||||
func (s *AnalyticsServiceTestSuite) TestIncrementWorkShares() {
|
||||
s.Run("should increment the share count for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Act
|
||||
err := s.service.IncrementWorkShares(context.Background(), work.ID)
|
||||
@ -129,7 +129,7 @@ func (s *AnalyticsServiceTestSuite) TestIncrementWorkShares() {
|
||||
func (s *AnalyticsServiceTestSuite) TestIncrementWorkTranslationCount() {
|
||||
s.Run("should increment the translation count for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
// Act
|
||||
err := s.service.IncrementWorkTranslationCount(context.Background(), work.ID)
|
||||
@ -145,7 +145,7 @@ func (s *AnalyticsServiceTestSuite) TestIncrementWorkTranslationCount() {
|
||||
func (s *AnalyticsServiceTestSuite) TestUpdateWorkReadingTime() {
|
||||
s.Run("should update the reading time for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
s.DB.Create(&domain.ReadabilityScore{WorkID: work.ID})
|
||||
s.DB.Create(&domain.LanguageAnalysis{WorkID: work.ID, Analysis: domain.JSONB{}})
|
||||
textMetadata := &domain.TextMetadata{
|
||||
@ -168,7 +168,7 @@ func (s *AnalyticsServiceTestSuite) TestUpdateWorkReadingTime() {
|
||||
func (s *AnalyticsServiceTestSuite) TestUpdateTranslationReadingTime() {
|
||||
s.Run("should update the reading time for a translation", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
translation := s.CreateTestTranslation(work.ID, "es", strings.Repeat("Contenido de prueba con quinientas palabras. ", 100))
|
||||
|
||||
// Act
|
||||
@ -185,7 +185,7 @@ func (s *AnalyticsServiceTestSuite) TestUpdateTranslationReadingTime() {
|
||||
func (s *AnalyticsServiceTestSuite) TestUpdateWorkComplexity() {
|
||||
s.Run("should update the complexity for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
s.DB.Create(&domain.TextMetadata{WorkID: work.ID})
|
||||
s.DB.Create(&domain.LanguageAnalysis{WorkID: work.ID, Analysis: domain.JSONB{}})
|
||||
readabilityScore := &domain.ReadabilityScore{
|
||||
@ -208,7 +208,7 @@ func (s *AnalyticsServiceTestSuite) TestUpdateWorkComplexity() {
|
||||
func (s *AnalyticsServiceTestSuite) TestUpdateWorkSentiment() {
|
||||
s.Run("should update the sentiment for a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
s.DB.Create(&domain.TextMetadata{WorkID: work.ID})
|
||||
s.DB.Create(&domain.ReadabilityScore{WorkID: work.ID})
|
||||
languageAnalysis := &domain.LanguageAnalysis{
|
||||
@ -233,7 +233,7 @@ func (s *AnalyticsServiceTestSuite) TestUpdateWorkSentiment() {
|
||||
func (s *AnalyticsServiceTestSuite) TestUpdateTranslationSentiment() {
|
||||
s.Run("should update the sentiment for a translation", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
translation := s.CreateTestTranslation(work.ID, "en", "This is a wonderfully positive and uplifting sentence.")
|
||||
|
||||
// Act
|
||||
@ -250,8 +250,8 @@ func (s *AnalyticsServiceTestSuite) TestUpdateTranslationSentiment() {
|
||||
func (s *AnalyticsServiceTestSuite) TestUpdateTrending() {
|
||||
s.Run("should update the trending works", func() {
|
||||
// Arrange
|
||||
work1 := s.CreateTestWork("Work 1", "en", "content")
|
||||
work2 := s.CreateTestWork("Work 2", "en", "content")
|
||||
work1 := s.CreateTestWork(s.AdminCtx, "Work 1", "en", "content")
|
||||
work2 := s.CreateTestWork(s.AdminCtx, "Work 2", "en", "content")
|
||||
s.DB.Create(&domain.WorkStats{WorkID: work1.ID, Views: 100, Likes: 10, Comments: 1})
|
||||
s.DB.Create(&domain.WorkStats{WorkID: work2.ID, Views: 10, Likes: 100, Comments: 10})
|
||||
|
||||
|
||||
@ -9,13 +9,17 @@ import (
|
||||
// Service provides authorization checks for the application.
|
||||
type Service struct {
|
||||
workRepo domain.WorkRepository
|
||||
authorRepo domain.AuthorRepository
|
||||
userRepo domain.UserRepository
|
||||
translationRepo domain.TranslationRepository
|
||||
}
|
||||
|
||||
// NewService creates a new authorization service.
|
||||
func NewService(workRepo domain.WorkRepository, translationRepo domain.TranslationRepository) *Service {
|
||||
func NewService(workRepo domain.WorkRepository, authorRepo domain.AuthorRepository, userRepo domain.UserRepository, translationRepo domain.TranslationRepository) *Service {
|
||||
return &Service{
|
||||
workRepo: workRepo,
|
||||
authorRepo: authorRepo,
|
||||
userRepo: userRepo,
|
||||
translationRepo: translationRepo,
|
||||
}
|
||||
}
|
||||
@ -33,8 +37,19 @@ func (s *Service) CanEditWork(ctx context.Context, userID uint, work *domain.Wor
|
||||
return true, nil
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
author, err := s.authorRepo.FindByName(ctx, user.Username)
|
||||
if err != nil {
|
||||
// If the author profile doesn't exist for the user, they can't be the author.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if the user is an author of the work.
|
||||
isAuthor, err := s.workRepo.IsAuthor(ctx, work.ID, userID)
|
||||
isAuthor, err := s.workRepo.IsAuthor(ctx, work.ID, author.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -46,14 +61,37 @@ func (s *Service) CanEditWork(ctx context.Context, userID uint, work *domain.Wor
|
||||
}
|
||||
|
||||
// CanDeleteWork checks if a user has permission to delete a work.
|
||||
func (s *Service) CanDeleteWork(ctx context.Context) (bool, error) {
|
||||
func (s *Service) CanDeleteWork(ctx context.Context, userID uint, work *domain.Work) (bool, error) {
|
||||
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return false, domain.ErrUnauthorized
|
||||
}
|
||||
|
||||
// Admins can do anything.
|
||||
if claims.Role == string(domain.UserRoleAdmin) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
author, err := s.authorRepo.FindByName(ctx, user.Username)
|
||||
if err != nil {
|
||||
// If the author profile doesn't exist for the user, they can't be the author.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Check if the user is an author of the work.
|
||||
isAuthor, err := s.workRepo.IsAuthor(ctx, work.ID, author.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if isAuthor {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, domain.ErrForbidden
|
||||
}
|
||||
|
||||
@ -76,7 +114,7 @@ func (s *Service) CanEditEntity(ctx context.Context, userID uint, translatableTy
|
||||
}
|
||||
|
||||
// CanDeleteTranslation checks if a user can delete a translation.
|
||||
func (s *Service) CanDeleteTranslation(ctx context.Context) (bool, error) {
|
||||
func (s *Service) CanDeleteTranslation(ctx context.Context, userID uint, translationID uint) (bool, error) {
|
||||
claims, ok := platform_auth.GetClaimsFromContext(ctx)
|
||||
if !ok {
|
||||
return false, domain.ErrUnauthorized
|
||||
@ -87,6 +125,15 @@ func (s *Service) CanDeleteTranslation(ctx context.Context) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
translation, err := s.translationRepo.GetByID(ctx, translationID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if translation.TranslatorID != nil && *translation.TranslatorID == userID {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, domain.ErrForbidden
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ func (s *CopyrightCommandsTestSuite) SetupSuite() {
|
||||
func (s *CopyrightCommandsTestSuite) TestAddCopyrightToWork() {
|
||||
s.Run("should add a copyright to a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
copyright := &domain.Copyright{Name: "Test Copyright", Identificator: "TC-123"}
|
||||
s.Require().NoError(s.CopyrightRepo.Create(context.Background(), copyright))
|
||||
|
||||
@ -47,7 +47,7 @@ func (s *CopyrightCommandsTestSuite) TestAddCopyrightToWork() {
|
||||
func (s *CopyrightCommandsTestSuite) TestRemoveCopyrightFromWork() {
|
||||
s.Run("should remove a copyright from a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "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))
|
||||
|
||||
@ -25,7 +25,7 @@ func (s *MonetizationCommandsTestSuite) SetupSuite() {
|
||||
func (s *MonetizationCommandsTestSuite) TestAddMonetizationToWork() {
|
||||
s.Run("should add a monetization to a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
monetization := &domain.Monetization{Amount: 10.0}
|
||||
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||
|
||||
|
||||
@ -100,7 +100,13 @@ func (c *TranslationCommands) CreateOrUpdateTranslation(ctx context.Context, inp
|
||||
func (c *TranslationCommands) DeleteTranslation(ctx context.Context, id uint) error {
|
||||
ctx, span := c.tracer.Start(ctx, "DeleteTranslation")
|
||||
defer span.End()
|
||||
can, err := c.authzSvc.CanDeleteTranslation(ctx)
|
||||
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
if !ok {
|
||||
return domain.ErrUnauthorized
|
||||
}
|
||||
|
||||
can, err := c.authzSvc.CanDeleteTranslation(ctx, userID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/app/translation"
|
||||
"tercul/internal/domain"
|
||||
@ -15,10 +16,146 @@ import (
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// MockAuthorRepository is a mock implementation of the AuthorRepository interface.
|
||||
type mockAuthorRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockAuthorRepository) Create(ctx context.Context, entity *domain.Author) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) Update(ctx context.Context, entity *domain.Author) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) Delete(ctx context.Context, id uint) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
args := m.Called(ctx, tx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Author]), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
args := m.Called(ctx, options)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) {
|
||||
args := m.Called(ctx, preloads, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, batchSize, offset)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(nil)
|
||||
}
|
||||
func (m *mockAuthorRepository) FindByName(ctx context.Context, name string) (*domain.Author, error) {
|
||||
args := m.Called(ctx, name)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, workID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, bookID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, countryID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
|
||||
type TranslationCommandsTestSuite struct {
|
||||
suite.Suite
|
||||
mockWorkRepo *testutil.MockWorkRepository
|
||||
mockTranslationRepo *testutil.MockTranslationRepository
|
||||
mockAuthorRepo *mockAuthorRepository
|
||||
mockUserRepo *testutil.MockUserRepository
|
||||
authzSvc *authz.Service
|
||||
cmd *translation.TranslationCommands
|
||||
adminCtx context.Context
|
||||
@ -30,11 +167,13 @@ type TranslationCommandsTestSuite struct {
|
||||
func (s *TranslationCommandsTestSuite) SetupTest() {
|
||||
s.mockWorkRepo = new(testutil.MockWorkRepository)
|
||||
s.mockTranslationRepo = new(testutil.MockTranslationRepository)
|
||||
s.authzSvc = authz.NewService(s.mockWorkRepo, s.mockTranslationRepo)
|
||||
s.mockAuthorRepo = new(mockAuthorRepository)
|
||||
s.mockUserRepo = new(testutil.MockUserRepository)
|
||||
s.authzSvc = authz.NewService(s.mockWorkRepo, s.mockAuthorRepo, s.mockUserRepo, s.mockTranslationRepo)
|
||||
s.cmd = translation.NewTranslationCommands(s.mockTranslationRepo, s.authzSvc)
|
||||
|
||||
s.adminUser = &domain.User{BaseModel: domain.BaseModel{ID: 1}, Role: domain.UserRoleAdmin}
|
||||
s.regularUser = &domain.User{BaseModel: domain.BaseModel{ID: 2}, Role: domain.UserRoleContributor}
|
||||
s.adminUser = &domain.User{BaseModel: domain.BaseModel{ID: 1}, Role: domain.UserRoleAdmin, Username: "admin"}
|
||||
s.regularUser = &domain.User{BaseModel: domain.BaseModel{ID: 2}, Role: domain.UserRoleContributor, Username: "contributor"}
|
||||
|
||||
s.adminCtx = context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{
|
||||
UserID: s.adminUser.ID,
|
||||
@ -50,7 +189,11 @@ func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() {
|
||||
testWork := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}},
|
||||
}
|
||||
input := translation.CreateOrUpdateTranslationInput{
|
||||
testAuthor := &domain.Author{
|
||||
TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}},
|
||||
Name: s.regularUser.Username,
|
||||
}
|
||||
baseInput := translation.CreateOrUpdateTranslationInput{
|
||||
Title: "Test Title",
|
||||
Content: "Test content",
|
||||
Language: "es",
|
||||
@ -59,6 +202,8 @@ func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() {
|
||||
}
|
||||
|
||||
s.Run("should create translation for admin", func() {
|
||||
s.SetupTest()
|
||||
input := baseInput
|
||||
// Arrange
|
||||
s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once()
|
||||
s.mockTranslationRepo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.Translation")).Return(nil).Once()
|
||||
@ -76,9 +221,13 @@ func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() {
|
||||
})
|
||||
|
||||
s.Run("should create translation for author", func() {
|
||||
s.SetupTest()
|
||||
input := baseInput
|
||||
// Arrange
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, s.regularUser.ID).Return(s.regularUser, nil).Once()
|
||||
s.mockAuthorRepo.On("FindByName", mock.Anything, s.regularUser.Username).Return(testAuthor, nil).Once()
|
||||
s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once()
|
||||
s.mockWorkRepo.On("IsAuthor", mock.Anything, testWork.ID, s.regularUser.ID).Return(true, nil).Once()
|
||||
s.mockWorkRepo.On("IsAuthor", mock.Anything, testWork.ID, testAuthor.ID).Return(true, nil).Once()
|
||||
s.mockTranslationRepo.On("Upsert", mock.Anything, mock.AnythingOfType("*domain.Translation")).Return(nil).Once()
|
||||
|
||||
// Act
|
||||
@ -89,14 +238,20 @@ func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() {
|
||||
s.NotNil(result)
|
||||
s.Equal(input.Title, result.Title)
|
||||
s.Equal(s.regularUser.ID, *result.TranslatorID)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
s.mockAuthorRepo.AssertExpectations(s.T())
|
||||
s.mockWorkRepo.AssertExpectations(s.T())
|
||||
s.mockTranslationRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("should fail if user is not authorized", func() {
|
||||
s.SetupTest()
|
||||
input := baseInput
|
||||
// Arrange
|
||||
s.mockUserRepo.On("GetByID", mock.Anything, s.regularUser.ID).Return(s.regularUser, nil).Once()
|
||||
s.mockAuthorRepo.On("FindByName", mock.Anything, s.regularUser.Username).Return(testAuthor, nil).Once()
|
||||
s.mockWorkRepo.On("GetByID", mock.Anything, testWork.ID).Return(testWork, nil).Once()
|
||||
s.mockWorkRepo.On("IsAuthor", mock.Anything, testWork.ID, s.regularUser.ID).Return(false, nil).Once()
|
||||
s.mockWorkRepo.On("IsAuthor", mock.Anything, testWork.ID, testAuthor.ID).Return(false, nil).Once()
|
||||
|
||||
// Act
|
||||
_, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, input)
|
||||
@ -104,16 +259,19 @@ func (s *TranslationCommandsTestSuite) TestCreateOrUpdateTranslation() {
|
||||
// Assert
|
||||
s.Error(err)
|
||||
s.ErrorIs(err, domain.ErrForbidden)
|
||||
s.mockUserRepo.AssertExpectations(s.T())
|
||||
s.mockAuthorRepo.AssertExpectations(s.T())
|
||||
s.mockWorkRepo.AssertExpectations(s.T())
|
||||
})
|
||||
|
||||
s.Run("should fail on validation error for empty language", func() {
|
||||
s.SetupTest()
|
||||
// Arrange
|
||||
invalidInput := input
|
||||
invalidInput.Language = ""
|
||||
input := baseInput
|
||||
input.Language = ""
|
||||
|
||||
// Act
|
||||
_, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, invalidInput)
|
||||
_, err := s.cmd.CreateOrUpdateTranslation(s.userCtx, input)
|
||||
|
||||
// Assert
|
||||
s.Error(err)
|
||||
|
||||
@ -2,6 +2,7 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"tercul/internal/app/authz"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
@ -20,9 +22,9 @@ type UserCommandsSuite struct {
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) SetupTest() {
|
||||
s.repo = &mockUserRepository{}
|
||||
workRepo := &mockWorkRepoForUserTests{}
|
||||
s.authzSvc = authz.NewService(workRepo, nil) // Translation repo not needed for user tests
|
||||
s.repo = new(mockUserRepository)
|
||||
// None of the repos are used by the authz checks in these command tests
|
||||
s.authzSvc = authz.NewService(nil, nil, nil, nil)
|
||||
s.commands = NewUserCommands(s.repo, s.authzSvc)
|
||||
}
|
||||
|
||||
@ -35,9 +37,8 @@ func (s *UserCommandsSuite) TestUpdateUser_Success_Self() {
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), 1)
|
||||
input := UpdateUserInput{ID: 1, Username: strPtr("new_username")}
|
||||
|
||||
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.User, error) {
|
||||
return &domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil
|
||||
}
|
||||
s.repo.On("GetByID", ctx, uint(1)).Return(&domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil)
|
||||
s.repo.On("Update", ctx, mock.AnythingOfType("*domain.User")).Return(nil)
|
||||
|
||||
// Act
|
||||
updatedUser, err := s.commands.UpdateUser(ctx, input)
|
||||
@ -46,6 +47,7 @@ func (s *UserCommandsSuite) TestUpdateUser_Success_Self() {
|
||||
assert.NoError(s.T(), err)
|
||||
assert.NotNil(s.T(), updatedUser)
|
||||
assert.Equal(s.T(), "new_username", updatedUser.Username)
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestUpdateUser_Success_Admin() {
|
||||
@ -53,9 +55,8 @@ func (s *UserCommandsSuite) TestUpdateUser_Success_Admin() {
|
||||
ctx := platform_auth.ContextWithAdminUser(context.Background(), 99) // Admin user
|
||||
input := UpdateUserInput{ID: 1, Username: strPtr("new_username_by_admin")}
|
||||
|
||||
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.User, error) {
|
||||
return &domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil
|
||||
}
|
||||
s.repo.On("GetByID", ctx, uint(1)).Return(&domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil)
|
||||
s.repo.On("Update", ctx, mock.AnythingOfType("*domain.User")).Return(nil)
|
||||
|
||||
// Act
|
||||
updatedUser, err := s.commands.UpdateUser(ctx, input)
|
||||
@ -64,6 +65,7 @@ func (s *UserCommandsSuite) TestUpdateUser_Success_Admin() {
|
||||
assert.NoError(s.T(), err)
|
||||
assert.NotNil(s.T(), updatedUser)
|
||||
assert.Equal(s.T(), "new_username_by_admin", updatedUser.Username)
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestUpdateUser_Forbidden() {
|
||||
@ -71,9 +73,7 @@ func (s *UserCommandsSuite) TestUpdateUser_Forbidden() {
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), 2) // Different user
|
||||
input := UpdateUserInput{ID: 1, Username: strPtr("forbidden_username")}
|
||||
|
||||
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.User, error) {
|
||||
return &domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil
|
||||
}
|
||||
// No need to mock GetByID, as the auth check happens first.
|
||||
|
||||
// Act
|
||||
_, err := s.commands.UpdateUser(ctx, input)
|
||||
@ -81,6 +81,7 @@ func (s *UserCommandsSuite) TestUpdateUser_Forbidden() {
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.ErrorIs(s.T(), err, domain.ErrForbidden)
|
||||
s.repo.AssertNotCalled(s.T(), "GetByID", mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestUpdateUser_Unauthorized() {
|
||||
@ -94,9 +95,220 @@ func (s *UserCommandsSuite) TestUpdateUser_Unauthorized() {
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.ErrorIs(s.T(), err, domain.ErrUnauthorized)
|
||||
s.repo.AssertNotCalled(s.T(), "GetByID", mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
// Helper to get a pointer to a string
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestCreateUser() {
|
||||
// Arrange
|
||||
ctx := context.Background()
|
||||
input := CreateUserInput{
|
||||
Username: "newuser",
|
||||
Email: "new@example.com",
|
||||
Password: "password",
|
||||
}
|
||||
s.repo.On("Create", ctx, mock.AnythingOfType("*domain.User")).Return(nil)
|
||||
|
||||
// Act
|
||||
user, err := s.commands.CreateUser(ctx, input)
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
assert.NotNil(s.T(), user)
|
||||
assert.Equal(s.T(), "newuser", user.Username)
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestDeleteUser_Success() {
|
||||
// Arrange
|
||||
ctx := platform_auth.ContextWithAdminUser(context.Background(), 99)
|
||||
s.repo.On("Delete", ctx, uint(1)).Return(nil)
|
||||
|
||||
// Act
|
||||
err := s.commands.DeleteUser(ctx, 1)
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestDeleteUser_Forbidden() {
|
||||
// Arrange
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), 2) // Non-admin user
|
||||
|
||||
// Act
|
||||
err := s.commands.DeleteUser(ctx, 1)
|
||||
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.ErrorIs(s.T(), err, domain.ErrForbidden)
|
||||
s.repo.AssertNotCalled(s.T(), "Delete", mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestUpdateUser_NotFound() {
|
||||
// Arrange
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), 1)
|
||||
input := UpdateUserInput{ID: 1, Username: strPtr("new_username")}
|
||||
|
||||
s.repo.On("GetByID", ctx, uint(1)).Return(nil, domain.ErrEntityNotFound)
|
||||
|
||||
// Act
|
||||
_, err := s.commands.UpdateUser(ctx, input)
|
||||
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.ErrorIs(s.T(), err, domain.ErrEntityNotFound)
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestCreateUser_Fails() {
|
||||
// Arrange
|
||||
ctx := context.Background()
|
||||
input := CreateUserInput{
|
||||
Username: "newuser",
|
||||
Email: "new@example.com",
|
||||
Password: "password",
|
||||
}
|
||||
s.repo.On("Create", ctx, mock.AnythingOfType("*domain.User")).Return(errors.New("db error"))
|
||||
|
||||
// Act
|
||||
_, err := s.commands.CreateUser(ctx, input)
|
||||
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.EqualError(s.T(), err, "db error")
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestDeleteUser_Unauthorized() {
|
||||
// Arrange
|
||||
ctx := context.Background() // No user in context
|
||||
|
||||
// Act
|
||||
err := s.commands.DeleteUser(ctx, 1)
|
||||
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.ErrorIs(s.T(), err, domain.ErrUnauthorized)
|
||||
s.repo.AssertNotCalled(s.T(), "Delete", mock.Anything, mock.Anything)
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestDeleteUser_AuthzFails() {
|
||||
// Arrange
|
||||
// This test requires a mock for the authz service, which is not currently mocked.
|
||||
// For now, this highlights a gap. To properly test this, we would need to
|
||||
// inject a mockable authz service.
|
||||
// Since the current authz service is a concrete implementation, we can't easily
|
||||
// simulate an error from `CanUpdateUser`. We will skip this test for now
|
||||
// as it requires a larger refactoring of the authz service dependency.
|
||||
s.T().Skip("Skipping test for authz failure as it requires mockable authz service")
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestUpdateUser_UpdateFails() {
|
||||
// Arrange
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), 1)
|
||||
input := UpdateUserInput{ID: 1, Username: strPtr("new_username")}
|
||||
testUser := &domain.User{BaseModel: domain.BaseModel{ID: 1}}
|
||||
|
||||
s.repo.On("GetByID", ctx, uint(1)).Return(testUser, nil)
|
||||
s.repo.On("Update", ctx, mock.AnythingOfType("*domain.User")).Return(errors.New("db error"))
|
||||
|
||||
// Act
|
||||
_, err := s.commands.UpdateUser(ctx, input)
|
||||
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.EqualError(s.T(), err, "db error")
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestUpdateUser_SetPasswordFails() {
|
||||
// Arrange
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), 1)
|
||||
emptyPassword := ""
|
||||
input := UpdateUserInput{ID: 1, Password: &emptyPassword}
|
||||
testUser := &domain.User{BaseModel: domain.BaseModel{ID: 1}}
|
||||
|
||||
s.repo.On("GetByID", ctx, uint(1)).Return(testUser, nil)
|
||||
|
||||
// Act
|
||||
_, err := s.commands.UpdateUser(ctx, input)
|
||||
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.EqualError(s.T(), err, "password cannot be empty")
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestUpdateUser_AllFields() {
|
||||
// Arrange
|
||||
ctx := platform_auth.ContextWithUserID(context.Background(), 1)
|
||||
countryID := uint(10)
|
||||
cityID := uint(20)
|
||||
addressID := uint(30)
|
||||
newRole := domain.UserRoleEditor
|
||||
verified := true
|
||||
active := false
|
||||
|
||||
input := UpdateUserInput{
|
||||
ID: 1,
|
||||
Username: strPtr("all_fields"),
|
||||
Email: strPtr("all@fields.com"),
|
||||
Password: strPtr("new_password"),
|
||||
FirstName: strPtr("First"),
|
||||
LastName: strPtr("Last"),
|
||||
DisplayName: strPtr("Display"),
|
||||
Bio: strPtr("Bio"),
|
||||
AvatarURL: strPtr("http://avatar.url"),
|
||||
Role: &newRole,
|
||||
Verified: &verified,
|
||||
Active: &active,
|
||||
CountryID: &countryID,
|
||||
CityID: &cityID,
|
||||
AddressID: &addressID,
|
||||
}
|
||||
|
||||
s.repo.On("GetByID", ctx, uint(1)).Return(&domain.User{BaseModel: domain.BaseModel{ID: 1}}, nil)
|
||||
s.repo.On("Update", ctx, mock.AnythingOfType("*domain.User")).Run(func(args mock.Arguments) {
|
||||
userArg := args.Get(1).(*domain.User)
|
||||
assert.Equal(s.T(), "all_fields", userArg.Username)
|
||||
assert.Equal(s.T(), "all@fields.com", userArg.Email)
|
||||
assert.True(s.T(), userArg.CheckPassword("new_password"))
|
||||
assert.Equal(s.T(), "First", userArg.FirstName)
|
||||
assert.Equal(s.T(), "Last", userArg.LastName)
|
||||
assert.Equal(s.T(), "Display", userArg.DisplayName)
|
||||
assert.Equal(s.T(), "Bio", userArg.Bio)
|
||||
assert.Equal(s.T(), "http://avatar.url", userArg.AvatarURL)
|
||||
assert.Equal(s.T(), newRole, userArg.Role)
|
||||
assert.Equal(s.T(), verified, userArg.Verified)
|
||||
assert.Equal(s.T(), active, userArg.Active)
|
||||
assert.Equal(s.T(), &countryID, userArg.CountryID)
|
||||
assert.Equal(s.T(), &cityID, userArg.CityID)
|
||||
assert.Equal(s.T(), &addressID, userArg.AddressID)
|
||||
}).Return(nil)
|
||||
|
||||
// Act
|
||||
_, err := s.commands.UpdateUser(ctx, input)
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserCommandsSuite) TestDeleteUser_NotFound() {
|
||||
// Arrange
|
||||
ctx := platform_auth.ContextWithAdminUser(context.Background(), 99)
|
||||
s.repo.On("Delete", ctx, uint(1)).Return(domain.ErrEntityNotFound)
|
||||
|
||||
// Act
|
||||
err := s.commands.DeleteUser(ctx, 1)
|
||||
|
||||
// Assert
|
||||
assert.Error(s.T(), err)
|
||||
assert.ErrorIs(s.T(), err, domain.ErrEntityNotFound)
|
||||
s.repo.AssertExpectations(s.T())
|
||||
}
|
||||
@ -3,30 +3,150 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// mockUserRepository is a mock implementation of the UserRepository
|
||||
type mockUserRepository struct {
|
||||
domain.UserRepository
|
||||
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)
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) Create(ctx context.Context, user *domain.User) error {
|
||||
if m.createFunc != nil {
|
||||
return m.createFunc(ctx, user)
|
||||
}
|
||||
return nil
|
||||
func (m *mockUserRepository) Create(ctx context.Context, entity *domain.User) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockUserRepository) Update(ctx context.Context, user *domain.User) error {
|
||||
if m.updateFunc != nil {
|
||||
return m.updateFunc(ctx, user)
|
||||
}
|
||||
return nil
|
||||
|
||||
func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
if m.getByIDFunc != nil {
|
||||
return m.getByIDFunc(ctx, id)
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return &domain.User{BaseModel: domain.BaseModel{ID: id}}, nil
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) Update(ctx context.Context, entity *domain.User) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) Delete(ctx context.Context, id uint) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
args := m.Called(ctx, tx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.User]), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) {
|
||||
args := m.Called(ctx, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) Count(ctx context.Context) (int64, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
args := m.Called(ctx, options)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) {
|
||||
args := m.Called(ctx, preloads, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) {
|
||||
args := m.Called(ctx, batchSize, offset)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
args := m.Called(ctx, fn)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||
args := m.Called(ctx, username)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
args := m.Called(ctx, email)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||
args := m.Called(ctx, role)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
278
internal/app/user/queries_test.go
Normal file
278
internal/app/user/queries_test.go
Normal file
@ -0,0 +1,278 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// mockUserProfileRepository is a mock implementation of the UserProfileRepository
|
||||
type mockUserProfileRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) GetByUserID(ctx context.Context, userID uint) (*domain.UserProfile, error) {
|
||||
args := m.Called(ctx, userID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.UserProfile), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) Create(ctx context.Context, entity *domain.UserProfile) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) GetByID(ctx context.Context, id uint) (*domain.UserProfile, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.UserProfile), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.UserProfile, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.UserProfile), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) Update(ctx context.Context, entity *domain.UserProfile) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.UserProfile) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) Delete(ctx context.Context, id uint) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
args := m.Called(ctx, tx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.UserProfile], error) {
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.UserProfile]), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.UserProfile, error) {
|
||||
args := m.Called(ctx, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.UserProfile), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) ListAll(ctx context.Context) ([]domain.UserProfile, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.UserProfile), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) Count(ctx context.Context) (int64, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
args := m.Called(ctx, options)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.UserProfile, error) {
|
||||
args := m.Called(ctx, preloads, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.UserProfile), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.UserProfile, error) {
|
||||
args := m.Called(ctx, batchSize, offset)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.UserProfile), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockUserProfileRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
args := m.Called(ctx, fn)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
type UserQueriesSuite struct {
|
||||
suite.Suite
|
||||
userRepo *mockUserRepository
|
||||
profileRepo *mockUserProfileRepository
|
||||
queries *UserQueries
|
||||
}
|
||||
|
||||
func (s *UserQueriesSuite) SetupTest() {
|
||||
s.userRepo = new(mockUserRepository)
|
||||
s.profileRepo = new(mockUserProfileRepository)
|
||||
s.queries = NewUserQueries(s.userRepo, s.profileRepo)
|
||||
}
|
||||
|
||||
func TestUserQueriesSuite(t *testing.T) {
|
||||
suite.Run(t, new(UserQueriesSuite))
|
||||
}
|
||||
|
||||
func (s *UserQueriesSuite) TestUser() {
|
||||
// Arrange
|
||||
ctx := context.Background()
|
||||
testUser := &domain.User{BaseModel: domain.BaseModel{ID: 1}, Username: "testuser"}
|
||||
|
||||
s.userRepo.On("GetByID", ctx, uint(1)).Return(testUser, nil)
|
||||
|
||||
// Act
|
||||
user, err := s.queries.User(ctx, 1)
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
assert.NotNil(s.T(), user)
|
||||
assert.Equal(s.T(), "testuser", user.Username)
|
||||
s.userRepo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserQueriesSuite) TestUserByUsername() {
|
||||
// Arrange
|
||||
ctx := context.Background()
|
||||
testUser := &domain.User{BaseModel: domain.BaseModel{ID: 1}, Username: "testuser"}
|
||||
|
||||
s.userRepo.On("FindByUsername", ctx, "testuser").Return(testUser, nil)
|
||||
|
||||
// Act
|
||||
user, err := s.queries.UserByUsername(ctx, "testuser")
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
assert.NotNil(s.T(), user)
|
||||
assert.Equal(s.T(), uint(1), user.ID)
|
||||
s.userRepo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserQueriesSuite) TestUserByEmail() {
|
||||
// Arrange
|
||||
ctx := context.Background()
|
||||
testUser := &domain.User{BaseModel: domain.BaseModel{ID: 1}, Email: "test@example.com"}
|
||||
|
||||
s.userRepo.On("FindByEmail", ctx, "test@example.com").Return(testUser, nil)
|
||||
|
||||
// Act
|
||||
user, err := s.queries.UserByEmail(ctx, "test@example.com")
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
assert.NotNil(s.T(), user)
|
||||
assert.Equal(s.T(), uint(1), user.ID)
|
||||
s.userRepo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserQueriesSuite) TestUsersByRole() {
|
||||
// Arrange
|
||||
ctx := context.Background()
|
||||
testUsers := []domain.User{
|
||||
{BaseModel: domain.BaseModel{ID: 1}, Role: domain.UserRoleAdmin},
|
||||
}
|
||||
|
||||
s.userRepo.On("ListByRole", ctx, domain.UserRoleAdmin).Return(testUsers, nil)
|
||||
|
||||
// Act
|
||||
users, err := s.queries.UsersByRole(ctx, domain.UserRoleAdmin)
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Len(s.T(), users, 1)
|
||||
s.userRepo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserQueriesSuite) TestUsers() {
|
||||
// Arrange
|
||||
ctx := context.Background()
|
||||
testUsers := []domain.User{
|
||||
{BaseModel: domain.BaseModel{ID: 1}},
|
||||
{BaseModel: domain.BaseModel{ID: 2}},
|
||||
}
|
||||
|
||||
s.userRepo.On("ListAll", ctx).Return(testUsers, nil)
|
||||
|
||||
// Act
|
||||
users, err := s.queries.Users(ctx)
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Len(s.T(), users, 2)
|
||||
s.userRepo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func (s *UserQueriesSuite) TestUserProfile() {
|
||||
// Arrange
|
||||
ctx := context.Background()
|
||||
testProfile := &domain.UserProfile{BaseModel: domain.BaseModel{ID: 1}, UserID: 1, Website: "https://example.com"}
|
||||
|
||||
s.profileRepo.On("GetByUserID", ctx, uint(1)).Return(testProfile, nil)
|
||||
|
||||
// Act
|
||||
profile, err := s.queries.UserProfile(ctx, 1)
|
||||
|
||||
// Assert
|
||||
assert.NoError(s.T(), err)
|
||||
assert.NotNil(s.T(), profile)
|
||||
assert.Equal(s.T(), "https://example.com", profile.Website)
|
||||
s.profileRepo.AssertExpectations(s.T())
|
||||
}
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
// Arrange
|
||||
userRepo := new(mockUserRepository)
|
||||
profileRepo := new(mockUserProfileRepository)
|
||||
authzSvc := authz.NewService(nil, nil, nil, nil)
|
||||
|
||||
// Act
|
||||
service := NewService(userRepo, authzSvc, profileRepo)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, service)
|
||||
assert.NotNil(t, service.Commands)
|
||||
assert.NotNil(t, service.Queries)
|
||||
}
|
||||
@ -19,6 +19,8 @@ import (
|
||||
// WorkCommands contains the command handlers for the work aggregate.
|
||||
type WorkCommands struct {
|
||||
repo domain.WorkRepository
|
||||
authorRepo domain.AuthorRepository
|
||||
userRepo domain.UserRepository
|
||||
searchClient search.SearchClient
|
||||
authzSvc *authz.Service
|
||||
analyticsSvc analytics.Service
|
||||
@ -26,9 +28,11 @@ type WorkCommands struct {
|
||||
}
|
||||
|
||||
// NewWorkCommands creates a new WorkCommands handler.
|
||||
func NewWorkCommands(repo domain.WorkRepository, searchClient search.SearchClient, authzSvc *authz.Service, analyticsSvc analytics.Service) *WorkCommands {
|
||||
func NewWorkCommands(repo domain.WorkRepository, authorRepo domain.AuthorRepository, userRepo domain.UserRepository, searchClient search.SearchClient, authzSvc *authz.Service, analyticsSvc analytics.Service) *WorkCommands {
|
||||
return &WorkCommands{
|
||||
repo: repo,
|
||||
authorRepo: authorRepo,
|
||||
userRepo: userRepo,
|
||||
searchClient: searchClient,
|
||||
authzSvc: authzSvc,
|
||||
analyticsSvc: analyticsSvc,
|
||||
@ -49,12 +53,48 @@ func (c *WorkCommands) CreateWork(ctx context.Context, work *domain.Work) (*doma
|
||||
if work.Language == "" {
|
||||
return nil, errors.New("work language cannot be empty")
|
||||
}
|
||||
err := c.repo.Create(ctx, work)
|
||||
|
||||
userID, ok := platform_auth.GetUserIDFromContext(ctx)
|
||||
if !ok {
|
||||
return nil, domain.ErrUnauthorized
|
||||
}
|
||||
|
||||
user, err := c.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user for author creation: %w", err)
|
||||
}
|
||||
|
||||
// Find or create an author for the user
|
||||
author, err := c.authorRepo.FindByName(ctx, user.Username)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrEntityNotFound) {
|
||||
// Author doesn't exist, create one
|
||||
newAuthor := &domain.Author{
|
||||
Name: user.Username,
|
||||
}
|
||||
if err := c.authorRepo.Create(ctx, newAuthor); err != nil {
|
||||
return nil, fmt.Errorf("failed to create author for user: %w", err)
|
||||
}
|
||||
author = newAuthor
|
||||
} else {
|
||||
// Another error occurred
|
||||
return nil, fmt.Errorf("failed to find author: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Associate the author with the work
|
||||
work.Authors = []*domain.Author{author}
|
||||
|
||||
err = c.repo.Create(ctx, work)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Index the work in the search client
|
||||
if err := c.searchClient.IndexWork(ctx, work, ""); err != nil {
|
||||
var content string
|
||||
if len(work.Translations) > 0 {
|
||||
content = work.Translations[0].Content
|
||||
}
|
||||
if err := c.searchClient.IndexWork(ctx, work, content); err != nil {
|
||||
// Log the error but don't fail the operation
|
||||
log.FromContext(ctx).Warn(fmt.Sprintf("Failed to index work after creation: %v", err))
|
||||
}
|
||||
@ -105,7 +145,7 @@ func (c *WorkCommands) UpdateWork(ctx context.Context, work *domain.Work) error
|
||||
return err
|
||||
}
|
||||
// Index the work in the search client
|
||||
return c.searchClient.IndexWork(ctx, work, "")
|
||||
return c.searchClient.IndexWork(ctx, work, work.Description)
|
||||
}
|
||||
|
||||
// DeleteWork deletes a work by ID after performing an authorization check.
|
||||
@ -129,7 +169,7 @@ func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
||||
return fmt.Errorf("failed to get work for authorization: %w", err)
|
||||
}
|
||||
|
||||
can, err := c.authzSvc.CanDeleteWork(ctx)
|
||||
can, err := c.authzSvc.CanDeleteWork(ctx, userID, existingWork)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -137,9 +177,6 @@ func (c *WorkCommands) DeleteWork(ctx context.Context, id uint) error {
|
||||
return domain.ErrForbidden
|
||||
}
|
||||
|
||||
_ = userID // to avoid unused variable error
|
||||
_ = existingWork // to avoid unused variable error
|
||||
|
||||
return c.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
|
||||
@ -1,414 +0,0 @@
|
||||
package work
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"tercul/internal/app/authz"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
platform_auth "tercul/internal/platform/auth"
|
||||
)
|
||||
|
||||
type WorkCommandsSuite struct {
|
||||
suite.Suite
|
||||
repo *mockWorkRepository
|
||||
searchClient *mockSearchClient
|
||||
authzSvc *authz.Service
|
||||
analyticsSvc *mockAnalyticsService
|
||||
commands *WorkCommands
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) SetupTest() {
|
||||
s.repo = &mockWorkRepository{}
|
||||
s.searchClient = &mockSearchClient{}
|
||||
s.authzSvc = authz.NewService(s.repo, nil)
|
||||
s.analyticsSvc = &mockAnalyticsService{}
|
||||
s.commands = NewWorkCommands(s.repo, s.searchClient, s.authzSvc, s.analyticsSvc)
|
||||
}
|
||||
|
||||
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() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
|
||||
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
return work, nil
|
||||
}
|
||||
s.repo.isAuthorFunc = func(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err := s.commands.UpdateWork(ctx, 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() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
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(ctx, work)
|
||||
assert.Error(s.T(), err)
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestUpdateWork_Forbidden() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 2, Role: string(domain.UserRoleReader)}) // Not an admin
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
|
||||
s.repo.isAuthorFunc = func(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||
return false, nil // User is not an author
|
||||
}
|
||||
|
||||
err := s.commands.UpdateWork(ctx, work)
|
||||
assert.Error(s.T(), err)
|
||||
assert.True(s.T(), errors.Is(err, domain.ErrForbidden))
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestUpdateWork_Unauthorized() {
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
err := s.commands.UpdateWork(context.Background(), work) // No user in context
|
||||
assert.Error(s.T(), err)
|
||||
assert.True(s.T(), errors.Is(err, domain.ErrUnauthorized))
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestDeleteWork_Success() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
work := &domain.Work{Title: "Test Work", TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
work.ID = 1
|
||||
|
||||
s.repo.getByIDFunc = func(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
return work, nil
|
||||
}
|
||||
s.repo.isAuthorFunc = func(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err := s.commands.DeleteWork(ctx, 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() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
s.repo.deleteFunc = func(ctx context.Context, id uint) error {
|
||||
return errors.New("db error")
|
||||
}
|
||||
err := s.commands.DeleteWork(ctx, 1)
|
||||
assert.Error(s.T(), err)
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestDeleteWork_Forbidden() {
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 2, Role: string(domain.UserRoleReader)}) // Not an admin
|
||||
err := s.commands.DeleteWork(ctx, 1)
|
||||
assert.Error(s.T(), err)
|
||||
assert.True(s.T(), errors.Is(err, domain.ErrForbidden))
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestDeleteWork_Unauthorized() {
|
||||
err := s.commands.DeleteWork(context.Background(), 1) // No user in context
|
||||
assert.Error(s.T(), err)
|
||||
assert.True(s.T(), errors.Is(err, domain.ErrUnauthorized))
|
||||
}
|
||||
|
||||
func (s *WorkCommandsSuite) TestAnalyzeWork_Success() {
|
||||
work := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: 1}},
|
||||
Translations: []*domain.Translation{
|
||||
{BaseModel: domain.BaseModel{ID: 101}},
|
||||
{BaseModel: domain.BaseModel{ID: 102}},
|
||||
},
|
||||
}
|
||||
s.repo.getWithTranslationsFunc = func(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
return work, nil
|
||||
}
|
||||
|
||||
var readingTime, complexity, sentiment, tReadingTime, tSentiment int
|
||||
s.analyticsSvc.updateWorkReadingTimeFunc = func(ctx context.Context, workID uint) error {
|
||||
readingTime++
|
||||
return nil
|
||||
}
|
||||
s.analyticsSvc.updateWorkComplexityFunc = func(ctx context.Context, workID uint) error {
|
||||
complexity++
|
||||
return nil
|
||||
}
|
||||
s.analyticsSvc.updateWorkSentimentFunc = func(ctx context.Context, workID uint) error {
|
||||
sentiment++
|
||||
return nil
|
||||
}
|
||||
s.analyticsSvc.updateTranslationReadingTimeFunc = func(ctx context.Context, translationID uint) error {
|
||||
tReadingTime++
|
||||
return nil
|
||||
}
|
||||
s.analyticsSvc.updateTranslationSentimentFunc = func(ctx context.Context, translationID uint) error {
|
||||
tSentiment++
|
||||
return nil
|
||||
}
|
||||
|
||||
err := s.commands.AnalyzeWork(context.Background(), 1)
|
||||
assert.NoError(s.T(), err)
|
||||
|
||||
assert.Equal(s.T(), 1, readingTime, "UpdateWorkReadingTime should be called once")
|
||||
assert.Equal(s.T(), 1, complexity, "UpdateWorkComplexity should be called once")
|
||||
assert.Equal(s.T(), 1, sentiment, "UpdateWorkSentiment should be called once")
|
||||
assert.Equal(s.T(), 2, tReadingTime, "UpdateTranslationReadingTime should be called for each translation")
|
||||
assert.Equal(s.T(), 2, tSentiment, "UpdateTranslationSentiment should be called for each translation")
|
||||
}
|
||||
|
||||
func TestMergeWork_Integration(t *testing.T) {
|
||||
// Setup in-memory SQLite DB
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Run migrations for all relevant tables
|
||||
err = db.AutoMigrate(
|
||||
&domain.Work{},
|
||||
&domain.Translation{},
|
||||
&domain.Author{},
|
||||
&domain.Tag{},
|
||||
&domain.Category{},
|
||||
&domain.Copyright{},
|
||||
&domain.Monetization{},
|
||||
&domain.WorkStats{},
|
||||
&domain.WorkAuthor{},
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create real repositories and services pointing to the test DB
|
||||
cfg, err := config.LoadConfig()
|
||||
assert.NoError(t, err)
|
||||
workRepo := sql.NewWorkRepository(db, cfg)
|
||||
authzSvc := authz.NewService(workRepo, nil) // Using real repo for authz checks
|
||||
searchClient := &mockSearchClient{} // Mock search client is fine
|
||||
analyticsSvc := &mockAnalyticsService{}
|
||||
commands := NewWorkCommands(workRepo, searchClient, authzSvc, analyticsSvc)
|
||||
|
||||
// Provide a realistic implementation for the GetOrCreateWorkStats mock
|
||||
analyticsSvc.getOrCreateWorkStatsFunc = func(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||
var stats domain.WorkStats
|
||||
if err := db.Where(domain.WorkStats{WorkID: workID}).FirstOrCreate(&stats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// --- Seed Data ---
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
author1 := &domain.Author{Name: "Author One"}
|
||||
db.Create(author1)
|
||||
author2 := &domain.Author{Name: "Author Two"}
|
||||
db.Create(author2)
|
||||
|
||||
tag1 := &domain.Tag{Name: "Tag One"}
|
||||
db.Create(tag1)
|
||||
tag2 := &domain.Tag{Name: "Tag Two"}
|
||||
db.Create(tag2)
|
||||
|
||||
sourceWork := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{Language: "en"},
|
||||
Title: "Source Work",
|
||||
Authors: []*domain.Author{author1},
|
||||
Tags: []*domain.Tag{tag1},
|
||||
}
|
||||
db.Create(sourceWork)
|
||||
db.Create(&domain.Translation{Title: "Source English", Language: "en", TranslatableID: sourceWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.Translation{Title: "Source French", Language: "fr", TranslatableID: sourceWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.WorkStats{WorkID: sourceWork.ID, Views: 10, Likes: 5})
|
||||
|
||||
targetWork := &domain.Work{
|
||||
TranslatableModel: domain.TranslatableModel{Language: "en"},
|
||||
Title: "Target Work",
|
||||
Authors: []*domain.Author{author2},
|
||||
Tags: []*domain.Tag{tag2},
|
||||
}
|
||||
db.Create(targetWork)
|
||||
db.Create(&domain.Translation{Title: "Target English", Language: "en", TranslatableID: targetWork.ID, TranslatableType: "works"})
|
||||
db.Create(&domain.WorkStats{WorkID: targetWork.ID, Views: 20, Likes: 10})
|
||||
|
||||
// --- Execute Merge ---
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
err = commands.MergeWork(ctx, sourceWork.ID, targetWork.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// --- Assertions ---
|
||||
// 1. Source work should be deleted
|
||||
var deletedWork domain.Work
|
||||
err = db.First(&deletedWork, sourceWork.ID).Error
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||
|
||||
// 2. Target work should have merged data
|
||||
var finalTargetWork domain.Work
|
||||
db.Preload("Translations").Preload("Authors").Preload("Tags").First(&finalTargetWork, targetWork.ID)
|
||||
|
||||
assert.Len(t, finalTargetWork.Translations, 2, "Should have two translations after merge")
|
||||
foundEn := false
|
||||
foundFr := false
|
||||
for _, tr := range finalTargetWork.Translations {
|
||||
if tr.Language == "en" {
|
||||
foundEn = true
|
||||
assert.Equal(t, "Target English", tr.Title, "Should keep target's English translation")
|
||||
}
|
||||
if tr.Language == "fr" {
|
||||
foundFr = true
|
||||
assert.Equal(t, "Source French", tr.Title, "Should merge source's French translation")
|
||||
}
|
||||
}
|
||||
assert.True(t, foundEn, "English translation should be present")
|
||||
assert.True(t, foundFr, "French translation should be present")
|
||||
|
||||
assert.Len(t, finalTargetWork.Authors, 2, "Authors should be merged")
|
||||
assert.Len(t, finalTargetWork.Tags, 2, "Tags should be merged")
|
||||
|
||||
// 3. Stats should be merged
|
||||
var finalStats domain.WorkStats
|
||||
db.Where("work_id = ?", targetWork.ID).First(&finalStats)
|
||||
assert.Equal(t, int64(30), finalStats.Views, "Views should be summed")
|
||||
assert.Equal(t, int64(15), finalStats.Likes, "Likes should be summed")
|
||||
|
||||
// 4. Source stats should be deleted
|
||||
var deletedStats domain.WorkStats
|
||||
err = db.First(&deletedStats, "work_id = ?", sourceWork.ID).Error
|
||||
assert.Error(t, err, "Source stats should be deleted")
|
||||
assert.True(t, errors.Is(err, gorm.ErrRecordNotFound))
|
||||
})
|
||||
|
||||
t.Run("Success with no target stats", func(t *testing.T) {
|
||||
sourceWork := &domain.Work{Title: "Source with Stats"}
|
||||
db.Create(sourceWork)
|
||||
db.Create(&domain.WorkStats{WorkID: sourceWork.ID, Views: 15, Likes: 7})
|
||||
|
||||
targetWork := &domain.Work{Title: "Target without Stats"}
|
||||
db.Create(targetWork)
|
||||
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
err := commands.MergeWork(ctx, sourceWork.ID, targetWork.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var finalStats domain.WorkStats
|
||||
db.Where("work_id = ?", targetWork.ID).First(&finalStats)
|
||||
assert.Equal(t, int64(15), finalStats.Views)
|
||||
assert.Equal(t, int64(7), finalStats.Likes)
|
||||
})
|
||||
|
||||
t.Run("Forbidden for non-admin", func(t *testing.T) {
|
||||
sourceWork := &domain.Work{Title: "Forbidden Source"}
|
||||
db.Create(sourceWork)
|
||||
targetWork := &domain.Work{Title: "Forbidden Target"}
|
||||
db.Create(targetWork)
|
||||
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 2, Role: string(domain.UserRoleReader)})
|
||||
err := commands.MergeWork(ctx, sourceWork.ID, targetWork.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, domain.ErrForbidden))
|
||||
})
|
||||
|
||||
t.Run("Source work not found", func(t *testing.T) {
|
||||
targetWork := &domain.Work{Title: "Existing Target"}
|
||||
db.Create(targetWork)
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
|
||||
err := commands.MergeWork(ctx, 99999, targetWork.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "entity not found")
|
||||
})
|
||||
|
||||
t.Run("Target work not found", func(t *testing.T) {
|
||||
sourceWork := &domain.Work{Title: "Existing Source"}
|
||||
db.Create(sourceWork)
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
|
||||
err := commands.MergeWork(ctx, sourceWork.ID, 99999)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "entity not found")
|
||||
})
|
||||
|
||||
t.Run("Cannot merge work into itself", func(t *testing.T) {
|
||||
work := &domain.Work{Title: "Self Merge Work"}
|
||||
db.Create(work)
|
||||
ctx := context.WithValue(context.Background(), platform_auth.ClaimsContextKey, &platform_auth.Claims{UserID: 1, Role: string(domain.UserRoleAdmin)})
|
||||
|
||||
err := commands.MergeWork(ctx, work.ID, work.ID)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "source and target work IDs cannot be the same")
|
||||
})
|
||||
}
|
||||
@ -3,106 +3,668 @@ package work
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
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)
|
||||
isAuthorFunc func(ctx context.Context, workID uint, authorID uint) (bool, error)
|
||||
listByCollectionIDFunc func(ctx context.Context, collectionID uint) ([]domain.Work, error)
|
||||
}
|
||||
// #region Mocks
|
||||
|
||||
func (m *mockWorkRepository) IsAuthor(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||
if m.isAuthorFunc != nil {
|
||||
return m.isAuthorFunc(ctx, workID, authorID)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
// mockWorkRepository is a mock implementation of domain.WorkRepository
|
||||
type mockWorkRepository struct{ mock.Mock }
|
||||
|
||||
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) Create(ctx context.Context, entity *domain.Work) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
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) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) GetByID(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
if m.getByIDFunc != nil {
|
||||
return m.getByIDFunc(ctx, id)
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return &domain.Work{TranslatableModel: domain.TranslatableModel{BaseModel: domain.BaseModel{ID: id}}}, nil
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) Update(ctx context.Context, entity *domain.Work) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Work) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) Delete(ctx context.Context, id uint) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockWorkRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
args := m.Called(ctx, tx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
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)
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return nil, nil
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Work]), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
if m.listByCollectionIDFunc != nil {
|
||||
return m.listByCollectionIDFunc(ctx, collectionID)
|
||||
func (m *mockWorkRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Work, error) {
|
||||
args := m.Called(ctx, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return nil, nil
|
||||
return args.Get(0).([]domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
if m.getWithTranslationsFunc != nil {
|
||||
return m.getWithTranslationsFunc(ctx, id)
|
||||
func (m *mockWorkRepository) ListAll(ctx context.Context) ([]domain.Work, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return nil, nil
|
||||
return args.Get(0).([]domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) Count(ctx context.Context) (int64, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
args := m.Called(ctx, options)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Work, error) {
|
||||
args := m.Called(ctx, preloads, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Work, error) {
|
||||
args := m.Called(ctx, batchSize, offset)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(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
|
||||
args := m.Called(ctx, title)
|
||||
return args.Get(0).([]domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) FindByAuthor(ctx context.Context, authorID uint) ([]domain.Work, error) {
|
||||
if m.findByAuthorFunc != nil {
|
||||
return m.findByAuthorFunc(ctx, authorID)
|
||||
}
|
||||
return nil, nil
|
||||
args := m.Called(ctx, authorID)
|
||||
return args.Get(0).([]domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) FindByCategory(ctx context.Context, categoryID uint) ([]domain.Work, error) {
|
||||
if m.findByCategoryFunc != nil {
|
||||
return m.findByCategoryFunc(ctx, categoryID)
|
||||
}
|
||||
return nil, nil
|
||||
args := m.Called(ctx, categoryID)
|
||||
return args.Get(0).([]domain.Work), args.Error(1)
|
||||
}
|
||||
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)
|
||||
args := m.Called(ctx, language, page, pageSize)
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Work]), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return nil, nil
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) IsAuthor(ctx context.Context, workID uint, authorID uint) (bool, error) {
|
||||
args := m.Called(ctx, workID, authorID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) ListByCollectionID(ctx context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
args := m.Called(ctx, collectionID)
|
||||
return args.Get(0).([]domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithAssociations(ctx context.Context, id uint) (*domain.Work, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.DB, id uint) (*domain.Work, error) {
|
||||
args := m.Called(ctx, tx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockWorkRepository) ListWithTranslations(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Work]), args.Error(1)
|
||||
}
|
||||
|
||||
type mockSearchClient struct {
|
||||
indexWorkFunc func(ctx context.Context, work *domain.Work, pipeline string) error
|
||||
// mockAuthorRepository is a mock implementation of domain.AuthorRepository
|
||||
type mockAuthorRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockAuthorRepository) Create(ctx context.Context, entity *domain.Author) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetByID(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Author, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) Update(ctx context.Context, entity *domain.Author) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Author) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) Delete(ctx context.Context, id uint) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
args := m.Called(ctx, tx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAuthorRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Author], error) {
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Author]), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListAll(ctx context.Context) ([]domain.Author, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) Count(ctx context.Context) (int64, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
args := m.Called(ctx, options)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Author, error) {
|
||||
args := m.Called(ctx, preloads, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, batchSize, offset)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(nil)
|
||||
}
|
||||
func (m *mockAuthorRepository) FindByName(ctx context.Context, name string) (*domain.Author, error) {
|
||||
args := m.Called(ctx, name)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, workID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, bookID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
||||
args := m.Called(ctx, countryID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Author), args.Error(1)
|
||||
}
|
||||
func (m *mockAuthorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Author), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *mockSearchClient) IndexWork(ctx context.Context, work *domain.Work, pipeline string) error {
|
||||
if m.indexWorkFunc != nil {
|
||||
return m.indexWorkFunc(ctx, work, pipeline)
|
||||
// mockUserRepository is a mock implementation of domain.UserRepository
|
||||
type mockUserRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockUserRepository) Create(ctx context.Context, entity *domain.User) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) Update(ctx context.Context, entity *domain.User) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockUserRepository) Delete(ctx context.Context, id uint) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
args := m.Called(ctx, tx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.User]), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) {
|
||||
args := m.Called(ctx, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) Count(ctx context.Context) (int64, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
args := m.Called(ctx, options)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) {
|
||||
args := m.Called(ctx, preloads, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) {
|
||||
args := m.Called(ctx, batchSize, offset)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(nil)
|
||||
}
|
||||
func (m *mockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||
args := m.Called(ctx, username)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
args := m.Called(ctx, email)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
func (m *mockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||
args := m.Called(ctx, role)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
// mockTranslationRepository is a mock implementation of domain.TranslationRepository
|
||||
type mockTranslationRepository struct{ mock.Mock }
|
||||
|
||||
func (m *mockTranslationRepository) Create(ctx context.Context, entity *domain.Translation) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockTranslationRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockTranslationRepository) GetByID(ctx context.Context, id uint) (*domain.Translation, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.Translation, error) {
|
||||
args := m.Called(ctx, id, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) Update(ctx context.Context, entity *domain.Translation) error {
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockTranslationRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.Translation) error {
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockTranslationRepository) Delete(ctx context.Context, id uint) error {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockTranslationRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
args := m.Called(ctx, tx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockTranslationRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Translation]), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.Translation, error) {
|
||||
args := m.Called(ctx, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) ListAll(ctx context.Context) ([]domain.Translation, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) Count(ctx context.Context) (int64, error) {
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
args := m.Called(ctx, options)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.Translation, error) {
|
||||
args := m.Called(ctx, preloads, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.Translation, error) {
|
||||
args := m.Called(ctx, batchSize, offset)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(nil)
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Translation, error) {
|
||||
args := m.Called(ctx, workID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByWorkIDPaginated(ctx context.Context, workID uint, language *string, page, pageSize int) (*domain.PaginatedResult[domain.Translation], error) {
|
||||
args := m.Called(ctx, workID, language, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.Translation]), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByEntity(ctx context.Context, entityType string, entityID uint) ([]domain.Translation, error) {
|
||||
args := m.Called(ctx, entityType, entityID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByTranslatorID(ctx context.Context, translatorID uint) ([]domain.Translation, error) {
|
||||
args := m.Called(ctx, translatorID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) ListByStatus(ctx context.Context, status domain.TranslationStatus) ([]domain.Translation, error) {
|
||||
args := m.Called(ctx, status)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.Translation), args.Error(1)
|
||||
}
|
||||
func (m *mockTranslationRepository) Upsert(ctx context.Context, translation *domain.Translation) error {
|
||||
args := m.Called(ctx, translation)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// mockSearchClient is a mock implementation of search.SearchClient
|
||||
type mockSearchClient struct{ mock.Mock }
|
||||
|
||||
func (m *mockSearchClient) IndexWork(ctx context.Context, work *domain.Work, content string) error {
|
||||
args := m.Called(ctx, work, content)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// mockAnalyticsService is a mock implementation of analytics.Service
|
||||
type mockAnalyticsService struct{ mock.Mock }
|
||||
|
||||
func (m *mockAnalyticsService) IncrementWorkViews(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkLikes(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
||||
args := m.Called(ctx, translationID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||
args := m.Called(ctx, translationID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) DecrementWorkLikes(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||
args := m.Called(ctx, translationID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
||||
args := m.Called(ctx, translationID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
||||
args := m.Called(ctx, translationID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||
args := m.Called(ctx, workID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.WorkStats), args.Error(1)
|
||||
}
|
||||
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||
args := m.Called(ctx, translationID)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.TranslationStats), args.Error(1)
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
||||
args := m.Called(ctx, workID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
|
||||
args := m.Called(ctx, translationID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
|
||||
args := m.Called(ctx, translationID)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
|
||||
args := m.Called(ctx, userID, eventType)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateTrending(ctx context.Context) error {
|
||||
args := m.Called(ctx)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
|
||||
args := m.Called(ctx, timePeriod, limit)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]*domain.Work), args.Error(1)
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||
args := m.Called(ctx, workID, stats)
|
||||
return args.Error(0)
|
||||
}
|
||||
func (m *mockAnalyticsService) MergeWorkStats(ctx context.Context, sourceWorkID, targetWorkID uint) error {
|
||||
args := m.Called(ctx, sourceWorkID, targetWorkID)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// #endregion Mocks
|
||||
@ -1,104 +1,4 @@
|
||||
package work
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tercul/internal/domain"
|
||||
)
|
||||
|
||||
type mockAnalyticsService struct {
|
||||
updateWorkReadingTimeFunc func(ctx context.Context, workID uint) error
|
||||
updateWorkComplexityFunc func(ctx context.Context, workID uint) error
|
||||
updateWorkSentimentFunc func(ctx context.Context, workID uint) error
|
||||
updateTranslationReadingTimeFunc func(ctx context.Context, translationID uint) error
|
||||
updateTranslationSentimentFunc func(ctx context.Context, translationID uint) error
|
||||
getOrCreateWorkStatsFunc func(ctx context.Context, workID uint) (*domain.WorkStats, error)
|
||||
updateWorkStatsFunc func(ctx context.Context, workID uint, stats domain.WorkStats) error
|
||||
}
|
||||
|
||||
func (m *mockAnalyticsService) UpdateWorkStats(ctx context.Context, workID uint, stats domain.WorkStats) error {
|
||||
if m.updateWorkStatsFunc != nil {
|
||||
return m.updateWorkStatsFunc(ctx, workID, stats)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAnalyticsService) UpdateWorkReadingTime(ctx context.Context, workID uint) error {
|
||||
if m.updateWorkReadingTimeFunc != nil {
|
||||
return m.updateWorkReadingTimeFunc(ctx, workID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAnalyticsService) UpdateWorkComplexity(ctx context.Context, workID uint) error {
|
||||
if m.updateWorkComplexityFunc != nil {
|
||||
return m.updateWorkComplexityFunc(ctx, workID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAnalyticsService) UpdateWorkSentiment(ctx context.Context, workID uint) error {
|
||||
if m.updateWorkSentimentFunc != nil {
|
||||
return m.updateWorkSentimentFunc(ctx, workID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAnalyticsService) UpdateTranslationReadingTime(ctx context.Context, translationID uint) error {
|
||||
if m.updateTranslationReadingTimeFunc != nil {
|
||||
return m.updateTranslationReadingTimeFunc(ctx, translationID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAnalyticsService) UpdateTranslationSentiment(ctx context.Context, translationID uint) error {
|
||||
if m.updateTranslationSentimentFunc != nil {
|
||||
return m.updateTranslationSentimentFunc(ctx, translationID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Implement other methods of the analytics.Service interface to satisfy the compiler
|
||||
func (m *mockAnalyticsService) IncrementWorkViews(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkComments(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkBookmarks(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementWorkShares(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) IncrementWorkTranslationCount(ctx context.Context, workID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationViews(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) DecrementWorkLikes(ctx context.Context, workID uint) error { return nil }
|
||||
func (m *mockAnalyticsService) DecrementTranslationLikes(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationComments(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) IncrementTranslationShares(ctx context.Context, translationID uint) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) GetOrCreateWorkStats(ctx context.Context, workID uint) (*domain.WorkStats, error) {
|
||||
if m.getOrCreateWorkStatsFunc != nil {
|
||||
return m.getOrCreateWorkStatsFunc(ctx, workID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAnalyticsService) GetOrCreateTranslationStats(ctx context.Context, translationID uint) (*domain.TranslationStats, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateUserEngagement(ctx context.Context, userID uint, eventType string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockAnalyticsService) UpdateTrending(ctx context.Context) error { return nil }
|
||||
func (m *mockAnalyticsService) GetTrendingWorks(ctx context.Context, timePeriod string, limit int) ([]*domain.Work, error) {
|
||||
return nil, nil
|
||||
}
|
||||
// This file is intentionally left empty.
|
||||
// Mocks are defined in main_test.go to avoid redeclaration errors.
|
||||
@ -2,10 +2,13 @@ package work
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"tercul/internal/domain"
|
||||
"testing"
|
||||
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type WorkQueriesSuite struct {
|
||||
@ -26,9 +29,8 @@ func TestWorkQueriesSuite(t *testing.T) {
|
||||
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
|
||||
}
|
||||
s.repo.On("GetByID", mock.Anything, uint(1)).Return(work, nil)
|
||||
|
||||
w, err := s.queries.GetWorkByID(context.Background(), 1)
|
||||
assert.NoError(s.T(), err)
|
||||
expectedDTO := &WorkDTO{
|
||||
@ -47,9 +49,7 @@ func (s *WorkQueriesSuite) TestGetWorkByID_ZeroID() {
|
||||
|
||||
func (s *WorkQueriesSuite) TestListByCollectionID_Success() {
|
||||
works := []domain.Work{{Title: "Test Work"}}
|
||||
s.repo.listByCollectionIDFunc = func(ctx context.Context, collectionID uint) ([]domain.Work, error) {
|
||||
return works, nil
|
||||
}
|
||||
s.repo.On("ListByCollectionID", mock.Anything, uint(1)).Return(works, nil)
|
||||
w, err := s.queries.ListByCollectionID(context.Background(), 1)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), works, w)
|
||||
@ -72,9 +72,7 @@ func (s *WorkQueriesSuite) TestListWorks_Success() {
|
||||
PageSize: 10,
|
||||
TotalPages: 1,
|
||||
}
|
||||
s.repo.listFunc = func(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.Work], error) {
|
||||
return domainWorks, nil
|
||||
}
|
||||
s.repo.On("List", mock.Anything, 1, 10).Return(domainWorks, nil)
|
||||
|
||||
paginatedDTOs, err := s.queries.ListWorks(context.Background(), 1, 10)
|
||||
assert.NoError(s.T(), err)
|
||||
@ -95,9 +93,7 @@ func (s *WorkQueriesSuite) TestListWorks_Success() {
|
||||
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
|
||||
}
|
||||
s.repo.On("GetWithTranslations", mock.Anything, uint(1)).Return(work, nil)
|
||||
w, err := s.queries.GetWorkWithTranslations(context.Background(), 1)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), work, w)
|
||||
@ -111,9 +107,7 @@ func (s *WorkQueriesSuite) TestGetWorkWithTranslations_ZeroID() {
|
||||
|
||||
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
|
||||
}
|
||||
s.repo.On("FindByTitle", mock.Anything, "Test").Return(works, nil)
|
||||
w, err := s.queries.FindWorksByTitle(context.Background(), "Test")
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), works, w)
|
||||
@ -127,9 +121,7 @@ func (s *WorkQueriesSuite) TestFindWorksByTitle_Empty() {
|
||||
|
||||
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
|
||||
}
|
||||
s.repo.On("FindByAuthor", mock.Anything, uint(1)).Return(works, nil)
|
||||
w, err := s.queries.FindWorksByAuthor(context.Background(), 1)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), works, w)
|
||||
@ -143,9 +135,7 @@ func (s *WorkQueriesSuite) TestFindWorksByAuthor_ZeroID() {
|
||||
|
||||
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
|
||||
}
|
||||
s.repo.On("FindByCategory", mock.Anything, uint(1)).Return(works, nil)
|
||||
w, err := s.queries.FindWorksByCategory(context.Background(), 1)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), works, w)
|
||||
@ -159,9 +149,7 @@ func (s *WorkQueriesSuite) TestFindWorksByCategory_ZeroID() {
|
||||
|
||||
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
|
||||
}
|
||||
s.repo.On("FindByLanguage", mock.Anything, "en", 1, 10).Return(works, nil)
|
||||
w, err := s.queries.FindWorksByLanguage(context.Background(), "en", 1, 10)
|
||||
assert.NoError(s.T(), err)
|
||||
assert.Equal(s.T(), works, w)
|
||||
|
||||
@ -14,9 +14,9 @@ type Service struct {
|
||||
}
|
||||
|
||||
// NewService creates a new work Service.
|
||||
func NewService(repo domain.WorkRepository, searchClient search.SearchClient, authzSvc *authz.Service, analyticsSvc analytics.Service) *Service {
|
||||
func NewService(repo domain.WorkRepository, authorRepo domain.AuthorRepository, userRepo domain.UserRepository, searchClient search.SearchClient, authzSvc *authz.Service, analyticsSvc analytics.Service) *Service {
|
||||
return &Service{
|
||||
Commands: NewWorkCommands(repo, searchClient, authzSvc, analyticsSvc),
|
||||
Commands: NewWorkCommands(repo, authorRepo, userRepo, searchClient, authzSvc, analyticsSvc),
|
||||
Queries: NewWorkQueries(repo),
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
package work
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"tercul/internal/app/authz"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
// Arrange
|
||||
mockRepo := &mockWorkRepository{}
|
||||
mockSearchClient := &mockSearchClient{}
|
||||
mockAuthzSvc := &authz.Service{}
|
||||
mockAnalyticsSvc := &mockAnalyticsService{}
|
||||
|
||||
// Act
|
||||
service := NewService(mockRepo, mockSearchClient, mockAuthzSvc, mockAnalyticsSvc)
|
||||
|
||||
// Assert
|
||||
assert.NotNil(t, service, "The new service should not be nil")
|
||||
assert.NotNil(t, service.Commands, "The service Commands should not be nil")
|
||||
assert.NotNil(t, service.Queries, "The service Queries should not be nil")
|
||||
}
|
||||
@ -201,4 +201,91 @@ func TestAnalyticsRepository_UpdateUserEngagement(t *testing.T) {
|
||||
|
||||
assert.Equal(t, 15, updatedEngagement.LikesGiven)
|
||||
assert.Equal(t, 10, updatedEngagement.WorksRead)
|
||||
}
|
||||
|
||||
func TestAnalyticsRepository_IncrementTranslationCounter(t *testing.T) {
|
||||
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup: Create a work and translation to associate stats with
|
||||
work := domain.Work{Title: "Test Work"}
|
||||
require.NoError(t, db.Create(&work).Error)
|
||||
translation := domain.Translation{Title: "Test Translation", TranslatableID: work.ID, TranslatableType: "works"}
|
||||
require.NoError(t, db.Create(&translation).Error)
|
||||
|
||||
t.Run("creates_new_stats_if_not_exist", func(t *testing.T) {
|
||||
err := repo.IncrementTranslationCounter(ctx, translation.ID, "views", 10)
|
||||
require.NoError(t, err)
|
||||
|
||||
var stats domain.TranslationStats
|
||||
err = db.Where("translation_id = ?", translation.ID).First(&stats).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(10), stats.Views)
|
||||
})
|
||||
|
||||
t.Run("increments_existing_stats", func(t *testing.T) {
|
||||
// Increment again
|
||||
err := repo.IncrementTranslationCounter(ctx, translation.ID, "views", 5)
|
||||
require.NoError(t, err)
|
||||
|
||||
var stats domain.TranslationStats
|
||||
err = db.Where("translation_id = ?", translation.ID).First(&stats).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(15), stats.Views) // 10 + 5
|
||||
})
|
||||
|
||||
t.Run("invalid_field", func(t *testing.T) {
|
||||
err := repo.IncrementTranslationCounter(ctx, translation.ID, "invalid_field", 1)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnalyticsRepository_UpdateWorkStats(t *testing.T) {
|
||||
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup
|
||||
work := domain.Work{Title: "Test Work"}
|
||||
require.NoError(t, db.Create(&work).Error)
|
||||
stats := domain.WorkStats{WorkID: work.ID, Views: 10}
|
||||
require.NoError(t, db.Create(&stats).Error)
|
||||
|
||||
// Act
|
||||
update := domain.WorkStats{ReadingTime: 120, Complexity: 0.5}
|
||||
err := repo.UpdateWorkStats(ctx, work.ID, update)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert
|
||||
var updatedStats domain.WorkStats
|
||||
err = db.Where("work_id = ?", work.ID).First(&updatedStats).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(10), updatedStats.Views) // Should not be zeroed
|
||||
assert.Equal(t, 120, updatedStats.ReadingTime)
|
||||
assert.Equal(t, 0.5, updatedStats.Complexity)
|
||||
}
|
||||
|
||||
func TestAnalyticsRepository_UpdateTranslationStats(t *testing.T) {
|
||||
repo, db := newTestAnalyticsRepoWithSQLite(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup
|
||||
work := domain.Work{Title: "Test Work"}
|
||||
require.NoError(t, db.Create(&work).Error)
|
||||
translation := domain.Translation{Title: "Test Translation", TranslatableID: work.ID, TranslatableType: "works"}
|
||||
require.NoError(t, db.Create(&translation).Error)
|
||||
stats := domain.TranslationStats{TranslationID: translation.ID, Views: 20}
|
||||
require.NoError(t, db.Create(&stats).Error)
|
||||
|
||||
// Act
|
||||
update := domain.TranslationStats{ReadingTime: 60, Sentiment: 0.8}
|
||||
err := repo.UpdateTranslationStats(ctx, translation.ID, update)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Assert
|
||||
var updatedStats domain.TranslationStats
|
||||
err = db.Where("translation_id = ?", translation.ID).First(&updatedStats).Error
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(20), updatedStats.Views) // Should not be zeroed
|
||||
assert.Equal(t, 60, updatedStats.ReadingTime)
|
||||
assert.Equal(t, 0.8, updatedStats.Sentiment)
|
||||
}
|
||||
@ -2,73 +2,66 @@ package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type authorRepository struct {
|
||||
domain.BaseRepository[domain.Author]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
*BaseRepositoryImpl[domain.Author]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAuthorRepository creates a new AuthorRepository.
|
||||
func NewAuthorRepository(db *gorm.DB, cfg *config.Config) domain.AuthorRepository {
|
||||
baseRepo := NewBaseRepositoryImpl[domain.Author](db, cfg)
|
||||
return &authorRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Author](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("author.repository"),
|
||||
BaseRepositoryImpl: baseRepo,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// ListByWorkID finds authors by work ID
|
||||
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||
ctx, span := r.tracer.Start(ctx, "ListByWorkID")
|
||||
defer span.End()
|
||||
var authors []domain.Author
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id").
|
||||
Where("work_authors.work_id = ?", workID).
|
||||
Find(&authors).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authors, nil
|
||||
}
|
||||
|
||||
// GetWithTranslations finds an author by ID and preloads their translations.
|
||||
func (r *authorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
ctx, span := r.tracer.Start(ctx, "GetWithTranslations")
|
||||
defer span.End()
|
||||
func (r *authorRepository) FindByName(ctx context.Context, name string) (*domain.Author, error) {
|
||||
var author domain.Author
|
||||
if err := r.db.WithContext(ctx).Preload("Translations").First(&author, id).Error; err != nil {
|
||||
err := r.db.WithContext(ctx).Where("name = ?", name).First(&author).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &author, nil
|
||||
}
|
||||
|
||||
// ListByBookID finds authors by book ID
|
||||
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
||||
ctx, span := r.tracer.Start(ctx, "ListByBookID")
|
||||
defer span.End()
|
||||
func (r *authorRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain.Author, error) {
|
||||
var authors []domain.Author
|
||||
if err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id").
|
||||
Where("book_authors.book_id = ?", bookID).
|
||||
Find(&authors).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authors, nil
|
||||
err := r.db.WithContext(ctx).Joins("JOIN work_authors ON work_authors.author_id = authors.id").
|
||||
Where("work_authors.work_id = ?", workID).Find(&authors).Error
|
||||
return authors, err
|
||||
}
|
||||
|
||||
// ListByCountryID finds authors by country ID
|
||||
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
||||
ctx, span := r.tracer.Start(ctx, "ListByCountryID")
|
||||
defer span.End()
|
||||
func (r *authorRepository) ListByBookID(ctx context.Context, bookID uint) ([]domain.Author, error) {
|
||||
var authors []domain.Author
|
||||
if err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error; err != nil {
|
||||
err := r.db.WithContext(ctx).Joins("JOIN book_authors ON book_authors.author_id = authors.id").
|
||||
Where("book_authors.book_id = ?", bookID).Find(&authors).Error
|
||||
return authors, err
|
||||
}
|
||||
|
||||
func (r *authorRepository) ListByCountryID(ctx context.Context, countryID uint) ([]domain.Author, error) {
|
||||
var authors []domain.Author
|
||||
err := r.db.WithContext(ctx).Where("country_id = ?", countryID).Find(&authors).Error
|
||||
return authors, err
|
||||
}
|
||||
|
||||
func (r *authorRepository) GetWithTranslations(ctx context.Context, id uint) (*domain.Author, error) {
|
||||
var author domain.Author
|
||||
err := r.db.WithContext(ctx).Preload("Translations").First(&author, id).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return authors, nil
|
||||
}
|
||||
return &author, nil
|
||||
}
|
||||
@ -24,9 +24,14 @@ func (s *AuthorRepositoryTestSuite) SetupSuite() {
|
||||
}
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.DB.Exec("DELETE FROM work_authors")
|
||||
s.DB.Exec("DELETE FROM authors")
|
||||
s.DB.Exec("DELETE FROM works")
|
||||
s.DB.Exec("DELETE FROM books")
|
||||
s.DB.Exec("DELETE FROM book_authors")
|
||||
s.DB.Exec("DELETE FROM countries")
|
||||
s.DB.Exec("DELETE FROM translations")
|
||||
}
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) createAuthor(name string) *domain.Author {
|
||||
@ -41,10 +46,17 @@ func (s *AuthorRepositoryTestSuite) createAuthor(name string) *domain.Author {
|
||||
return author
|
||||
}
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) createBook(title string) *domain.Book {
|
||||
book := &domain.Book{Title: title, TranslatableModel: domain.TranslatableModel{Language: "en"}}
|
||||
err := s.DB.Create(book).Error
|
||||
s.Require().NoError(err)
|
||||
return book
|
||||
}
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) TestListByWorkID() {
|
||||
s.Run("should return all authors for a given work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
author1 := s.createAuthor("Author 1")
|
||||
author2 := s.createAuthor("Author 2")
|
||||
s.Require().NoError(s.DB.Model(&work).Association("Authors").Append([]*domain.Author{author1, author2}))
|
||||
@ -52,10 +64,83 @@ func (s *AuthorRepositoryTestSuite) TestListByWorkID() {
|
||||
// Act
|
||||
authors, err := s.AuthorRepo.ListByWorkID(context.Background(), work.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(authors, 3)
|
||||
var authorNames []string
|
||||
for _, a := range authors {
|
||||
authorNames = append(authorNames, a.Name)
|
||||
}
|
||||
s.ElementsMatch([]string{"admin", "Author 1", "Author 2"}, authorNames)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) TestListByBookID() {
|
||||
s.Run("should return all authors for a given book", func() {
|
||||
// Arrange
|
||||
book := s.createBook("Test Book")
|
||||
author1 := s.createAuthor("Book Author 1")
|
||||
author2 := s.createAuthor("Book Author 2")
|
||||
s.Require().NoError(s.DB.Model(&book).Association("Authors").Append([]*domain.Author{author1, author2}))
|
||||
|
||||
// Act
|
||||
authors, err := s.AuthorRepo.ListByBookID(context.Background(), book.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(authors, 2)
|
||||
s.ElementsMatch([]string{"Author 1", "Author 2"}, []string{authors[0].Name, authors[1].Name})
|
||||
s.ElementsMatch([]string{"Book Author 1", "Book Author 2"}, []string{authors[0].Name, authors[1].Name})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) TestListByCountryID() {
|
||||
s.Run("should return all authors for a given country", func() {
|
||||
// Arrange
|
||||
country1 := &domain.Country{Name: "Country 1", Code: "C1"}
|
||||
country2 := &domain.Country{Name: "Country 2", Code: "C2"}
|
||||
s.Require().NoError(s.DB.Create(country1).Error)
|
||||
s.Require().NoError(s.DB.Create(country2).Error)
|
||||
|
||||
author1 := s.createAuthor("Author C1")
|
||||
author1.CountryID = &country1.ID
|
||||
s.Require().NoError(s.DB.Save(author1).Error)
|
||||
|
||||
author2 := s.createAuthor("Author C2")
|
||||
author2.CountryID = &country2.ID
|
||||
s.Require().NoError(s.DB.Save(author2).Error)
|
||||
|
||||
// Act
|
||||
authors, err := s.AuthorRepo.ListByCountryID(context.Background(), country1.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(authors, 1)
|
||||
s.Equal("Author C1", authors[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *AuthorRepositoryTestSuite) TestGetWithTranslations() {
|
||||
s.Run("should return author with preloaded translations", func() {
|
||||
// Arrange
|
||||
author := s.createAuthor("Translated Author")
|
||||
translation := &domain.Translation{
|
||||
TranslatableType: "authors",
|
||||
TranslatableID: author.ID,
|
||||
Language: "es",
|
||||
Title: "Autor Traducido",
|
||||
Content: "Una biografía.",
|
||||
}
|
||||
s.Require().NoError(s.DB.Create(translation).Error)
|
||||
|
||||
// Act
|
||||
foundAuthor, err := s.AuthorRepo.GetWithTranslations(context.Background(), author.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(foundAuthor)
|
||||
s.Require().Len(foundAuthor.Translations, 1)
|
||||
s.Equal("es", foundAuthor.Translations[0].Language)
|
||||
s.Equal("Una biografía.", foundAuthor.Translations[0].Content)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -14,16 +14,6 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Common repository errors
|
||||
var (
|
||||
ErrEntityNotFound = errors.New("entity not found")
|
||||
ErrInvalidID = errors.New("invalid ID: cannot be zero")
|
||||
ErrInvalidInput = errors.New("invalid input parameters")
|
||||
ErrDatabaseOperation = errors.New("database operation failed")
|
||||
ErrContextRequired = errors.New("context is required")
|
||||
ErrTransactionFailed = errors.New("transaction failed")
|
||||
)
|
||||
|
||||
// BaseRepositoryImpl provides a default implementation of BaseRepository using GORM
|
||||
type BaseRepositoryImpl[T any] struct {
|
||||
db *gorm.DB
|
||||
@ -43,7 +33,7 @@ func NewBaseRepositoryImpl[T any](db *gorm.DB, cfg *config.Config) *BaseReposito
|
||||
// validateContext ensures context is not nil
|
||||
func (r *BaseRepositoryImpl[T]) validateContext(ctx context.Context) error {
|
||||
if ctx == nil {
|
||||
return ErrContextRequired
|
||||
return domain.ErrValidation
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -51,7 +41,7 @@ func (r *BaseRepositoryImpl[T]) validateContext(ctx context.Context) error {
|
||||
// validateID ensures ID is valid
|
||||
func (r *BaseRepositoryImpl[T]) validateID(id uint) error {
|
||||
if id == 0 {
|
||||
return ErrInvalidID
|
||||
return domain.ErrValidation
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -59,7 +49,7 @@ func (r *BaseRepositoryImpl[T]) validateID(id uint) error {
|
||||
// validateEntity ensures entity is not nil
|
||||
func (r *BaseRepositoryImpl[T]) validateEntity(entity *T) error {
|
||||
if entity == nil {
|
||||
return ErrInvalidInput
|
||||
return domain.ErrValidation
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -133,7 +123,7 @@ func (r *BaseRepositoryImpl[T]) Create(ctx context.Context, entity *T) error {
|
||||
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to create entity")
|
||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("Entity created successfully in %s", duration))
|
||||
@ -151,7 +141,7 @@ func (r *BaseRepositoryImpl[T]) CreateInTx(ctx context.Context, tx *gorm.DB, ent
|
||||
return err
|
||||
}
|
||||
if tx == nil {
|
||||
return ErrTransactionFailed
|
||||
return domain.ErrInvalidOperation
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
@ -160,7 +150,7 @@ func (r *BaseRepositoryImpl[T]) CreateInTx(ctx context.Context, tx *gorm.DB, ent
|
||||
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to create entity in transaction")
|
||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("Entity created successfully in transaction in %s", duration))
|
||||
@ -186,10 +176,10 @@ func (r *BaseRepositoryImpl[T]) GetByID(ctx context.Context, id uint) (*T, error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Debug(fmt.Sprintf("Entity with id %d not found in %s", id, duration))
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
log.Error(err, fmt.Sprintf("Failed to get entity by ID %d", id))
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("Entity with id %d retrieved successfully in %s", id, duration))
|
||||
@ -216,10 +206,10 @@ func (r *BaseRepositoryImpl[T]) GetByIDWithOptions(ctx context.Context, id uint,
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Debug(fmt.Sprintf("Entity with id %d not found with options in %s", id, duration))
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
log.Error(err, fmt.Sprintf("Failed to get entity by ID %d with options", id))
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("Entity with id %d retrieved successfully with options in %s", id, duration))
|
||||
@ -243,7 +233,7 @@ func (r *BaseRepositoryImpl[T]) Update(ctx context.Context, entity *T) error {
|
||||
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to update entity")
|
||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("Entity updated successfully in %s", duration))
|
||||
@ -261,7 +251,7 @@ func (r *BaseRepositoryImpl[T]) UpdateInTx(ctx context.Context, tx *gorm.DB, ent
|
||||
return err
|
||||
}
|
||||
if tx == nil {
|
||||
return ErrTransactionFailed
|
||||
return domain.ErrInvalidOperation
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
@ -270,7 +260,7 @@ func (r *BaseRepositoryImpl[T]) UpdateInTx(ctx context.Context, tx *gorm.DB, ent
|
||||
|
||||
if err != nil {
|
||||
log.Error(err, "Failed to update entity in transaction")
|
||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("Entity updated successfully in transaction in %s", duration))
|
||||
@ -295,12 +285,12 @@ func (r *BaseRepositoryImpl[T]) Delete(ctx context.Context, id uint) error {
|
||||
|
||||
if result.Error != nil {
|
||||
log.Error(result.Error, fmt.Sprintf("Failed to delete entity with id %d", id))
|
||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, result.Error)
|
||||
return fmt.Errorf("database operation failed: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
log.Debug(fmt.Sprintf("No entity with id %d found to delete in %s", id, duration))
|
||||
return ErrEntityNotFound
|
||||
return domain.ErrEntityNotFound
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("Entity with id %d deleted successfully in %s", id, duration))
|
||||
@ -318,7 +308,7 @@ func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id
|
||||
return err
|
||||
}
|
||||
if tx == nil {
|
||||
return ErrTransactionFailed
|
||||
return domain.ErrInvalidOperation
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
@ -328,12 +318,12 @@ func (r *BaseRepositoryImpl[T]) DeleteInTx(ctx context.Context, tx *gorm.DB, id
|
||||
|
||||
if result.Error != nil {
|
||||
log.Error(result.Error, fmt.Sprintf("Failed to delete entity with id %d in transaction", id))
|
||||
return fmt.Errorf("%w: %v", ErrDatabaseOperation, result.Error)
|
||||
return fmt.Errorf("database operation failed: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
log.Debug(fmt.Sprintf("No entity with id %d found to delete in transaction in %s", id, duration))
|
||||
return ErrEntityNotFound
|
||||
return domain.ErrEntityNotFound
|
||||
}
|
||||
|
||||
log.Debug(fmt.Sprintf("Entity with id %d deleted successfully in transaction in %s", id, duration))
|
||||
@ -360,7 +350,7 @@ func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*
|
||||
// Get total count
|
||||
if err := r.db.WithContext(ctx).Model(new(T)).Count(&totalCount).Error; err != nil {
|
||||
log.Error(err, "Failed to count entities")
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
// Calculate offset
|
||||
@ -369,7 +359,7 @@ func (r *BaseRepositoryImpl[T]) List(ctx context.Context, page, pageSize int) (*
|
||||
// Get paginated data
|
||||
if err := r.db.WithContext(ctx).Offset(offset).Limit(pageSize).Find(&entities).Error; err != nil {
|
||||
log.Error(err, "Failed to get paginated entities")
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@ -410,7 +400,7 @@ func (r *BaseRepositoryImpl[T]) ListWithOptions(ctx context.Context, options *do
|
||||
|
||||
if err := query.Find(&entities).Error; err != nil {
|
||||
log.Error(err, "Failed to get entities with options")
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@ -431,7 +421,7 @@ func (r *BaseRepositoryImpl[T]) ListAll(ctx context.Context) ([]T, error) {
|
||||
var entities []T
|
||||
if err := r.db.WithContext(ctx).Find(&entities).Error; err != nil {
|
||||
log.Error(err, "Failed to get all entities")
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@ -452,7 +442,7 @@ func (r *BaseRepositoryImpl[T]) Count(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(new(T)).Count(&count).Error; err != nil {
|
||||
log.Error(err, "Failed to count entities")
|
||||
return 0, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return 0, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@ -475,7 +465,7 @@ func (r *BaseRepositoryImpl[T]) CountWithOptions(ctx context.Context, options *d
|
||||
|
||||
if err := query.Model(new(T)).Count(&count).Error; err != nil {
|
||||
log.Error(err, "Failed to count entities with options")
|
||||
return 0, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return 0, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@ -506,10 +496,10 @@ func (r *BaseRepositoryImpl[T]) FindWithPreload(ctx context.Context, preloads []
|
||||
if err := query.First(&entity, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Debug(fmt.Sprintf("Entity with id %d not found with preloads in %s", id, time.Since(start)))
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
log.Error(err, fmt.Sprintf("Failed to get entity with id %d with preloads", id))
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@ -541,7 +531,7 @@ func (r *BaseRepositoryImpl[T]) GetAllForSync(ctx context.Context, batchSize, of
|
||||
var entities []T
|
||||
if err := r.db.WithContext(ctx).Offset(offset).Limit(batchSize).Find(&entities).Error; err != nil {
|
||||
log.Error(err, "Failed to get entities for sync")
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@ -565,7 +555,7 @@ func (r *BaseRepositoryImpl[T]) Exists(ctx context.Context, id uint) (bool, erro
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id).Count(&count).Error; err != nil {
|
||||
log.Error(err, fmt.Sprintf("Failed to check entity existence for id %d", id))
|
||||
return false, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return false, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(start)
|
||||
@ -587,7 +577,7 @@ func (r *BaseRepositoryImpl[T]) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
tx := r.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
log.Error(tx.Error, "Failed to begin transaction")
|
||||
return nil, fmt.Errorf("%w: %v", ErrTransactionFailed, tx.Error)
|
||||
return nil, fmt.Errorf("transaction failed: %w", tx.Error)
|
||||
}
|
||||
|
||||
log.Debug("Transaction started successfully")
|
||||
@ -625,9 +615,9 @@ func (r *BaseRepositoryImpl[T]) WithTx(ctx context.Context, fn func(tx *gorm.DB)
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
log.Error(err, "Failed to commit transaction")
|
||||
return fmt.Errorf("%w: %v", ErrTransactionFailed, err)
|
||||
return fmt.Errorf("transaction failed: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("Transaction committed successfully")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,7 @@ func (s *BaseRepositoryTestSuite) SetupSuite() {
|
||||
|
||||
// SetupTest cleans the database before each test.
|
||||
func (s *BaseRepositoryTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.DB.Exec("DELETE FROM test_entities")
|
||||
}
|
||||
|
||||
@ -76,13 +77,13 @@ func (s *BaseRepositoryTestSuite) TestCreate() {
|
||||
|
||||
s.Run("should return error for nil entity", func() {
|
||||
err := s.repo.Create(context.Background(), nil)
|
||||
s.ErrorIs(err, sql.ErrInvalidInput)
|
||||
s.ErrorIs(err, domain.ErrValidation)
|
||||
})
|
||||
|
||||
s.Run("should return error for nil context", func() {
|
||||
//nolint:staticcheck // Testing behavior with nil context is intentional here.
|
||||
err := s.repo.Create(nil, &testutil.TestEntity{Name: "Test Context"})
|
||||
s.ErrorIs(err, sql.ErrContextRequired)
|
||||
s.ErrorIs(err, domain.ErrValidation)
|
||||
})
|
||||
}
|
||||
|
||||
@ -103,12 +104,12 @@ func (s *BaseRepositoryTestSuite) TestGetByID() {
|
||||
|
||||
s.Run("should return ErrEntityNotFound for non-existent ID", func() {
|
||||
_, err := s.repo.GetByID(context.Background(), 99999)
|
||||
s.ErrorIs(err, sql.ErrEntityNotFound)
|
||||
s.ErrorIs(err, domain.ErrEntityNotFound)
|
||||
})
|
||||
|
||||
s.Run("should return ErrInvalidID for zero ID", func() {
|
||||
s.Run("should return ErrValidation for zero ID", func() {
|
||||
_, err := s.repo.GetByID(context.Background(), 0)
|
||||
s.ErrorIs(err, sql.ErrInvalidID)
|
||||
s.ErrorIs(err, domain.ErrValidation)
|
||||
})
|
||||
}
|
||||
|
||||
@ -140,12 +141,12 @@ func (s *BaseRepositoryTestSuite) TestDelete() {
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
_, getErr := s.repo.GetByID(context.Background(), created.ID)
|
||||
s.ErrorIs(getErr, sql.ErrEntityNotFound)
|
||||
s.ErrorIs(getErr, domain.ErrEntityNotFound)
|
||||
})
|
||||
|
||||
s.Run("should return ErrEntityNotFound when deleting non-existent entity", func() {
|
||||
err := s.repo.Delete(context.Background(), 99999)
|
||||
s.ErrorIs(err, sql.ErrEntityNotFound)
|
||||
s.ErrorIs(err, domain.ErrEntityNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
@ -261,6 +262,6 @@ func (s *BaseRepositoryTestSuite) TestWithTx() {
|
||||
s.ErrorIs(err, simulatedErr)
|
||||
|
||||
_, getErr := s.repo.GetByID(context.Background(), createdID)
|
||||
s.ErrorIs(getErr, sql.ErrEntityNotFound, "Entity should not exist after rollback")
|
||||
s.ErrorIs(getErr, domain.ErrEntityNotFound, "Entity should not exist after rollback")
|
||||
})
|
||||
}
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type bookRepository struct {
|
||||
domain.BaseRepository[domain.Book]
|
||||
*BaseRepositoryImpl[domain.Book]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -20,9 +20,9 @@ type bookRepository struct {
|
||||
// NewBookRepository creates a new BookRepository.
|
||||
func NewBookRepository(db *gorm.DB, cfg *config.Config) domain.BookRepository {
|
||||
return &bookRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Book](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("book.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.Book](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("book.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,9 +70,9 @@ func (r *bookRepository) FindByISBN(ctx context.Context, isbn string) (*domain.B
|
||||
var book domain.Book
|
||||
if err := r.db.WithContext(ctx).Where("isbn = ?", isbn).First(&book).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &book, nil
|
||||
}
|
||||
}
|
||||
@ -24,7 +24,13 @@ func (s *BookRepositoryTestSuite) SetupSuite() {
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.DB.Exec("DELETE FROM books")
|
||||
s.DB.Exec("DELETE FROM authors")
|
||||
s.DB.Exec("DELETE FROM publishers")
|
||||
s.DB.Exec("DELETE FROM book_authors")
|
||||
s.DB.Exec("DELETE FROM book_works")
|
||||
s.DB.Exec("DELETE FROM works")
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) createBook(title, isbn string) *domain.Book {
|
||||
@ -40,6 +46,20 @@ func (s *BookRepositoryTestSuite) createBook(title, isbn string) *domain.Book {
|
||||
return book
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) createAuthor(name string) *domain.Author {
|
||||
author := &domain.Author{Name: name}
|
||||
err := s.DB.Create(author).Error
|
||||
s.Require().NoError(err)
|
||||
return author
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) createPublisher(name string) *domain.Publisher {
|
||||
publisher := &domain.Publisher{Name: name}
|
||||
err := s.DB.Create(publisher).Error
|
||||
s.Require().NoError(err)
|
||||
return publisher
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) TestFindByISBN() {
|
||||
s.Run("should return a book by ISBN", func() {
|
||||
// Arrange
|
||||
@ -68,4 +88,76 @@ func (s *BookRepositoryTestSuite) TestFindByISBN() {
|
||||
|
||||
func TestBookRepository(t *testing.T) {
|
||||
suite.Run(t, new(BookRepositoryTestSuite))
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) TestListByAuthorID() {
|
||||
s.Run("should return all books for a given author", func() {
|
||||
// Arrange
|
||||
author1 := s.createAuthor("Test Author 1")
|
||||
author2 := s.createAuthor("Test Author 2")
|
||||
book1 := s.createBook("Book 1 by Author 1", "111")
|
||||
book2 := s.createBook("Book 2 by Author 1", "222")
|
||||
book3 := s.createBook("Book 3 by Author 2", "333")
|
||||
|
||||
s.Require().NoError(s.DB.Model(&author1).Association("Books").Append([]*domain.Book{book1, book2}))
|
||||
s.Require().NoError(s.DB.Model(&author2).Association("Books").Append(book3))
|
||||
|
||||
// Act
|
||||
books, err := s.BookRepo.ListByAuthorID(context.Background(), author1.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(books, 2)
|
||||
s.ElementsMatch([]string{"Book 1 by Author 1", "Book 2 by Author 1"}, []string{books[0].Title, books[1].Title})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) TestListByPublisherID() {
|
||||
s.Run("should return all books for a given publisher", func() {
|
||||
// Arrange
|
||||
publisher1 := s.createPublisher("Publisher 1")
|
||||
publisher2 := s.createPublisher("Publisher 2")
|
||||
book1 := s.createBook("Book 1 from Publisher 1", "111")
|
||||
book2 := s.createBook("Book 2 from Publisher 1", "222")
|
||||
book3 := s.createBook("Book 3 from Publisher 2", "333")
|
||||
|
||||
book1.PublisherID = &publisher1.ID
|
||||
book2.PublisherID = &publisher1.ID
|
||||
book3.PublisherID = &publisher2.ID
|
||||
s.Require().NoError(s.DB.Save(book1).Error)
|
||||
s.Require().NoError(s.DB.Save(book2).Error)
|
||||
s.Require().NoError(s.DB.Save(book3).Error)
|
||||
|
||||
// Act
|
||||
books, err := s.BookRepo.ListByPublisherID(context.Background(), publisher1.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(books, 2)
|
||||
s.ElementsMatch([]string{"Book 1 from Publisher 1", "Book 2 from Publisher 1"}, []string{books[0].Title, books[1].Title})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *BookRepositoryTestSuite) TestListByWorkID() {
|
||||
s.Run("should return all books associated with a given work", func() {
|
||||
// Arrange
|
||||
work1 := s.CreateTestWork(s.AdminCtx, "Work 1", "en", "content 1")
|
||||
work2 := s.CreateTestWork(s.AdminCtx, "Work 2", "en", "content 2")
|
||||
book1 := s.createBook("Book 1 for Work 1", "111")
|
||||
book2 := s.createBook("Book 2 for Work 1", "222")
|
||||
book3 := s.createBook("Book 3 for Work 2", "333")
|
||||
|
||||
// Manually create the association in the join table
|
||||
s.Require().NoError(s.DB.Exec("INSERT INTO book_works (book_id, work_id) VALUES (?, ?)", book1.ID, work1.ID).Error)
|
||||
s.Require().NoError(s.DB.Exec("INSERT INTO book_works (book_id, work_id) VALUES (?, ?)", book2.ID, work1.ID).Error)
|
||||
s.Require().NoError(s.DB.Exec("INSERT INTO book_works (book_id, work_id) VALUES (?, ?)", book3.ID, work2.ID).Error)
|
||||
|
||||
// Act
|
||||
books, err := s.BookRepo.ListByWorkID(context.Background(), work1.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(books, 2)
|
||||
s.ElementsMatch([]string{"Book 1 for Work 1", "Book 2 for Work 1"}, []string{books[0].Title, books[1].Title})
|
||||
})
|
||||
}
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type categoryRepository struct {
|
||||
domain.BaseRepository[domain.Category]
|
||||
*BaseRepositoryImpl[domain.Category]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -20,9 +20,9 @@ type categoryRepository struct {
|
||||
// NewCategoryRepository creates a new CategoryRepository.
|
||||
func NewCategoryRepository(db *gorm.DB, cfg *config.Config) domain.CategoryRepository {
|
||||
return &categoryRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Category](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("category.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.Category](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("category.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func (r *categoryRepository) FindByName(ctx context.Context, name string) (*doma
|
||||
var category domain.Category
|
||||
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&category).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@ -68,4 +68,4 @@ func (r *categoryRepository) ListByParentID(ctx context.Context, parentID *uint)
|
||||
}
|
||||
}
|
||||
return categories, nil
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,7 @@ func (s *CategoryRepositoryTestSuite) SetupSuite() {
|
||||
}
|
||||
|
||||
func (s *CategoryRepositoryTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.DB.Exec("DELETE FROM work_categories")
|
||||
s.DB.Exec("DELETE FROM categories")
|
||||
s.DB.Exec("DELETE FROM works")
|
||||
@ -64,7 +65,7 @@ func (s *CategoryRepositoryTestSuite) TestFindByName() {
|
||||
func (s *CategoryRepositoryTestSuite) TestListByWorkID() {
|
||||
s.Run("should return all categories for a given work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
cat1 := s.createCategory("Science Fiction", nil)
|
||||
cat2 := s.createCategory("Cyberpunk", &cat1.ID)
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type copyrightRepository struct {
|
||||
domain.BaseRepository[domain.Copyright]
|
||||
*BaseRepositoryImpl[domain.Copyright]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -20,9 +20,9 @@ type copyrightRepository struct {
|
||||
// NewCopyrightRepository creates a new CopyrightRepository.
|
||||
func NewCopyrightRepository(db *gorm.DB, cfg *config.Config) domain.CopyrightRepository {
|
||||
return ©rightRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Copyright](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("copyright.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.Copyright](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("copyright.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,7 @@ func (r *copyrightRepository) GetTranslationByLanguage(ctx context.Context, copy
|
||||
err := r.db.WithContext(ctx).Where("copyright_id = ? AND language_code = ?", copyrightID, languageCode).First(&translation).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@ -115,4 +115,4 @@ func (r *copyrightRepository) RemoveCopyrightFromSource(ctx context.Context, sou
|
||||
ctx, span := r.tracer.Start(ctx, "RemoveCopyrightFromSource")
|
||||
defer span.End()
|
||||
return r.db.WithContext(ctx).Exec("DELETE FROM source_copyrights WHERE source_id = ? AND copyright_id = ?", sourceID, copyrightID).Error
|
||||
}
|
||||
}
|
||||
@ -10,15 +10,15 @@ import (
|
||||
)
|
||||
|
||||
type countryRepository struct {
|
||||
domain.BaseRepository[domain.Country]
|
||||
*BaseRepositoryImpl[domain.Country]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewCountryRepository creates a new CountryRepository.
|
||||
func NewCountryRepository(db *gorm.DB, cfg *config.Config) domain.CountryRepository {
|
||||
return &countryRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Country](db, cfg),
|
||||
db: db,
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.Country](db, cfg),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func (r *countryRepository) GetByCode(ctx context.Context, code string) (*domain
|
||||
var country domain.Country
|
||||
if err := r.db.WithContext(ctx).Where("code = ?", code).First(&country).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@ -41,4 +41,4 @@ func (r *countryRepository) ListByContinent(ctx context.Context, continent strin
|
||||
return nil, err
|
||||
}
|
||||
return countries, nil
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type editionRepository struct {
|
||||
domain.BaseRepository[domain.Edition]
|
||||
*BaseRepositoryImpl[domain.Edition]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -20,9 +20,9 @@ type editionRepository struct {
|
||||
// NewEditionRepository creates a new EditionRepository.
|
||||
func NewEditionRepository(db *gorm.DB, cfg *config.Config) domain.EditionRepository {
|
||||
return &editionRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Edition](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("edition.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.Edition](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("edition.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,9 +44,9 @@ func (r *editionRepository) FindByISBN(ctx context.Context, isbn string) (*domai
|
||||
var edition domain.Edition
|
||||
if err := r.db.WithContext(ctx).Where("isbn = ?", isbn).First(&edition).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &edition, nil
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type emailVerificationRepository struct {
|
||||
domain.BaseRepository[domain.EmailVerification]
|
||||
*BaseRepositoryImpl[domain.EmailVerification]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -21,9 +21,9 @@ type emailVerificationRepository struct {
|
||||
// NewEmailVerificationRepository creates a new EmailVerificationRepository.
|
||||
func NewEmailVerificationRepository(db *gorm.DB, cfg *config.Config) domain.EmailVerificationRepository {
|
||||
return &emailVerificationRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.EmailVerification](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("email_verification.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.EmailVerification](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("email_verification.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ func (r *emailVerificationRepository) GetByToken(ctx context.Context, token stri
|
||||
var verification domain.EmailVerification
|
||||
if err := r.db.WithContext(ctx).Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&verification).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
129
internal/data/sql/like_repository_test.go
Normal file
129
internal/data/sql/like_repository_test.go
Normal file
@ -0,0 +1,129 @@
|
||||
package sql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type LikeRepositoryTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
LikeRepo domain.LikeRepository
|
||||
UserRepo domain.UserRepository
|
||||
WorkRepo domain.WorkRepository
|
||||
}
|
||||
|
||||
func (s *LikeRepositoryTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.LikeRepo = sql.NewLikeRepository(s.DB, cfg)
|
||||
s.UserRepo = sql.NewUserRepository(s.DB, cfg)
|
||||
s.WorkRepo = sql.NewWorkRepository(s.DB, cfg)
|
||||
}
|
||||
|
||||
func (s *LikeRepositoryTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.DB.Exec("DELETE FROM likes")
|
||||
s.DB.Exec("DELETE FROM works")
|
||||
}
|
||||
|
||||
func TestLikeRepository(t *testing.T) {
|
||||
suite.Run(t, new(LikeRepositoryTestSuite))
|
||||
}
|
||||
|
||||
func (s *LikeRepositoryTestSuite) createUser(username string) *domain.User {
|
||||
user := &domain.User{Username: username, Email: username + "@test.com"}
|
||||
err := s.UserRepo.Create(context.Background(), user)
|
||||
s.Require().NoError(err)
|
||||
return user
|
||||
}
|
||||
|
||||
func (s *LikeRepositoryTestSuite) TestListByUserID() {
|
||||
s.Run("should return all likes for a given user", func() {
|
||||
// Arrange
|
||||
user1 := s.createUser("user1")
|
||||
user2 := s.createUser("user2")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, WorkID: &work.ID}))
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, WorkID: &work.ID}))
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user2.ID, WorkID: &work.ID}))
|
||||
|
||||
// Act
|
||||
likes, err := s.LikeRepo.ListByUserID(context.Background(), user1.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(likes, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *LikeRepositoryTestSuite) TestListByWorkID() {
|
||||
s.Run("should return all likes for a given work", func() {
|
||||
// Arrange
|
||||
user1 := s.createUser("user1")
|
||||
work1 := s.CreateTestWork(s.AdminCtx, "Test Work 1", "en", "Test content")
|
||||
work2 := s.CreateTestWork(s.AdminCtx, "Test Work 2", "en", "Test content")
|
||||
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, WorkID: &work1.ID}))
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, WorkID: &work1.ID}))
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, WorkID: &work2.ID}))
|
||||
|
||||
// Act
|
||||
likes, err := s.LikeRepo.ListByWorkID(context.Background(), work1.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(likes, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *LikeRepositoryTestSuite) TestListByTranslationID() {
|
||||
s.Run("should return all likes for a given translation", func() {
|
||||
// Arrange
|
||||
user1 := s.createUser("user1")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
translation1 := s.CreateTestTranslation(work.ID, "es", "Contenido de prueba")
|
||||
translation2 := s.CreateTestTranslation(work.ID, "fr", "Contenu de test")
|
||||
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, TranslationID: &translation1.ID}))
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, TranslationID: &translation1.ID}))
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, TranslationID: &translation2.ID}))
|
||||
|
||||
// Act
|
||||
likes, err := s.LikeRepo.ListByTranslationID(context.Background(), translation1.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(likes, 2)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *LikeRepositoryTestSuite) TestListByCommentID() {
|
||||
s.Run("should return all likes for a given comment", func() {
|
||||
// Arrange
|
||||
user1 := s.createUser("user1")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
comment1 := &domain.Comment{UserID: user1.ID, WorkID: &work.ID, Text: "Comment 1"}
|
||||
comment2 := &domain.Comment{UserID: user1.ID, WorkID: &work.ID, Text: "Comment 2"}
|
||||
s.Require().NoError(s.DB.Create(comment1).Error)
|
||||
s.Require().NoError(s.DB.Create(comment2).Error)
|
||||
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, CommentID: &comment1.ID}))
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, CommentID: &comment1.ID}))
|
||||
s.Require().NoError(s.LikeRepo.Create(context.Background(), &domain.Like{UserID: user1.ID, CommentID: &comment2.ID}))
|
||||
|
||||
// Act
|
||||
likes, err := s.LikeRepo.ListByCommentID(context.Background(), comment1.ID)
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Len(likes, 2)
|
||||
})
|
||||
}
|
||||
@ -24,6 +24,7 @@ func (s *MonetizationRepositoryTestSuite) SetupSuite() {
|
||||
}
|
||||
|
||||
func (s *MonetizationRepositoryTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.DB.Exec("DELETE FROM work_monetizations")
|
||||
s.DB.Exec("DELETE FROM monetizations")
|
||||
s.DB.Exec("DELETE FROM works")
|
||||
@ -32,7 +33,7 @@ func (s *MonetizationRepositoryTestSuite) SetupTest() {
|
||||
func (s *MonetizationRepositoryTestSuite) TestAddMonetizationToWork() {
|
||||
s.Run("should add a monetization to a work", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
monetization := &domain.Monetization{Amount: 10.0}
|
||||
s.Require().NoError(s.DB.Create(monetization).Error)
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type passwordResetRepository struct {
|
||||
domain.BaseRepository[domain.PasswordReset]
|
||||
*BaseRepositoryImpl[domain.PasswordReset]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -21,9 +21,9 @@ type passwordResetRepository struct {
|
||||
// NewPasswordResetRepository creates a new PasswordResetRepository.
|
||||
func NewPasswordResetRepository(db *gorm.DB, cfg *config.Config) domain.PasswordResetRepository {
|
||||
return &passwordResetRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.PasswordReset](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("password_reset.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.PasswordReset](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("password_reset.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ func (r *passwordResetRepository) GetByToken(ctx context.Context, token string)
|
||||
var reset domain.PasswordReset
|
||||
if err := r.db.WithContext(ctx).Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).First(&reset).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type sourceRepository struct {
|
||||
domain.BaseRepository[domain.Source]
|
||||
*BaseRepositoryImpl[domain.Source]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -20,9 +20,9 @@ type sourceRepository struct {
|
||||
// NewSourceRepository creates a new SourceRepository.
|
||||
func NewSourceRepository(db *gorm.DB, cfg *config.Config) domain.SourceRepository {
|
||||
return &sourceRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Source](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("source.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.Source](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("source.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,9 +46,9 @@ func (r *sourceRepository) FindByURL(ctx context.Context, url string) (*domain.S
|
||||
var source domain.Source
|
||||
if err := r.db.WithContext(ctx).Where("url = ?", url).First(&source).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &source, nil
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type tagRepository struct {
|
||||
domain.BaseRepository[domain.Tag]
|
||||
*BaseRepositoryImpl[domain.Tag]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -20,9 +20,9 @@ type tagRepository struct {
|
||||
// NewTagRepository creates a new TagRepository.
|
||||
func NewTagRepository(db *gorm.DB, cfg *config.Config) domain.TagRepository {
|
||||
return &tagRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.Tag](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("tag.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.Tag](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("tag.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func (r *tagRepository) FindByName(ctx context.Context, name string) (*domain.Ta
|
||||
var tag domain.Tag
|
||||
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&tag).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@ -51,4 +51,4 @@ func (r *tagRepository) ListByWorkID(ctx context.Context, workID uint) ([]domain
|
||||
return nil, err
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type userProfileRepository struct {
|
||||
domain.BaseRepository[domain.UserProfile]
|
||||
*BaseRepositoryImpl[domain.UserProfile]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -20,9 +20,9 @@ type userProfileRepository struct {
|
||||
// NewUserProfileRepository creates a new UserProfileRepository.
|
||||
func NewUserProfileRepository(db *gorm.DB, cfg *config.Config) domain.UserProfileRepository {
|
||||
return &userProfileRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.UserProfile](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user_profile.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.UserProfile](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user_profile.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,9 +33,9 @@ func (r *userProfileRepository) GetByUserID(ctx context.Context, userID uint) (*
|
||||
var profile domain.UserProfile
|
||||
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).First(&profile).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &profile, nil
|
||||
}
|
||||
}
|
||||
@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
type userRepository struct {
|
||||
domain.BaseRepository[domain.User]
|
||||
*BaseRepositoryImpl[domain.User]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -20,9 +20,9 @@ type userRepository struct {
|
||||
// NewUserRepository creates a new UserRepository.
|
||||
func NewUserRepository(db *gorm.DB, cfg *config.Config) domain.UserRepository {
|
||||
return &userRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.User](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.User](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ func (r *userRepository) FindByUsername(ctx context.Context, username string) (*
|
||||
var user domain.User
|
||||
if err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@ -47,7 +47,7 @@ func (r *userRepository) FindByEmail(ctx context.Context, email string) (*domain
|
||||
var user domain.User
|
||||
if err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@ -63,4 +63,4 @@ func (r *userRepository) ListByRole(ctx context.Context, role domain.UserRole) (
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
}
|
||||
114
internal/data/sql/user_repository_test.go
Normal file
114
internal/data/sql/user_repository_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package sql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"tercul/internal/data/sql"
|
||||
"tercul/internal/domain"
|
||||
"tercul/internal/platform/config"
|
||||
"tercul/internal/testutil"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type UserRepositoryTestSuite struct {
|
||||
testutil.IntegrationTestSuite
|
||||
UserRepo domain.UserRepository
|
||||
}
|
||||
|
||||
func (s *UserRepositoryTestSuite) SetupSuite() {
|
||||
s.IntegrationTestSuite.SetupSuite(testutil.DefaultTestConfig())
|
||||
cfg, err := config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
s.UserRepo = sql.NewUserRepository(s.DB, cfg)
|
||||
}
|
||||
|
||||
func (s *UserRepositoryTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.DB.Exec("DELETE FROM users")
|
||||
}
|
||||
|
||||
func TestUserRepository(t *testing.T) {
|
||||
suite.Run(t, new(UserRepositoryTestSuite))
|
||||
}
|
||||
|
||||
func (s *UserRepositoryTestSuite) createUser(username, email string, role domain.UserRole) *domain.User {
|
||||
user := &domain.User{
|
||||
Username: username,
|
||||
Email: email,
|
||||
Role: role,
|
||||
}
|
||||
err := s.UserRepo.Create(context.Background(), user)
|
||||
s.Require().NoError(err)
|
||||
return user
|
||||
}
|
||||
|
||||
func (s *UserRepositoryTestSuite) TestFindByUsername() {
|
||||
s.Run("should find a user by username", func() {
|
||||
// Arrange
|
||||
expectedUser := s.createUser("testuser", "test@test.com", domain.UserRoleReader)
|
||||
|
||||
// Act
|
||||
foundUser, err := s.UserRepo.FindByUsername(context.Background(), "testuser")
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(foundUser)
|
||||
s.Equal(expectedUser.ID, foundUser.ID)
|
||||
s.Equal("testuser", foundUser.Username)
|
||||
})
|
||||
|
||||
s.Run("should return error if user not found", func() {
|
||||
_, err := s.UserRepo.FindByUsername(context.Background(), "nonexistent")
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrEntityNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserRepositoryTestSuite) TestFindByEmail() {
|
||||
s.Run("should find a user by email", func() {
|
||||
// Arrange
|
||||
expectedUser := s.createUser("testuser", "test@test.com", domain.UserRoleReader)
|
||||
|
||||
// Act
|
||||
foundUser, err := s.UserRepo.FindByEmail(context.Background(), "test@test.com")
|
||||
|
||||
// Assert
|
||||
s.Require().NoError(err)
|
||||
s.Require().NotNil(foundUser)
|
||||
s.Equal(expectedUser.ID, foundUser.ID)
|
||||
s.Equal("test@test.com", foundUser.Email)
|
||||
})
|
||||
|
||||
s.Run("should return error if user not found", func() {
|
||||
_, err := s.UserRepo.FindByEmail(context.Background(), "nonexistent@test.com")
|
||||
s.Require().Error(err)
|
||||
s.ErrorIs(err, domain.ErrEntityNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserRepositoryTestSuite) TestListByRole() {
|
||||
s.Run("should return all users for a given role", func() {
|
||||
// Arrange
|
||||
s.createUser("reader1", "reader1@test.com", domain.UserRoleReader)
|
||||
s.createUser("reader2", "reader2@test.com", domain.UserRoleReader)
|
||||
s.createUser("admin1", "admin1@test.com", domain.UserRoleAdmin)
|
||||
|
||||
// Act
|
||||
readers, err := s.UserRepo.ListByRole(context.Background(), domain.UserRoleReader)
|
||||
s.Require().NoError(err)
|
||||
|
||||
admins, err := s.UserRepo.ListByRole(context.Background(), domain.UserRoleAdmin)
|
||||
s.Require().NoError(err)
|
||||
|
||||
// Assert
|
||||
s.Len(readers, 2)
|
||||
s.Len(admins, 1)
|
||||
})
|
||||
|
||||
s.Run("should return empty slice if no users for role", func() {
|
||||
users, err := s.UserRepo.ListByRole(context.Background(), domain.UserRoleContributor)
|
||||
s.Require().NoError(err)
|
||||
s.Len(users, 0)
|
||||
})
|
||||
}
|
||||
@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type userSessionRepository struct {
|
||||
domain.BaseRepository[domain.UserSession]
|
||||
*BaseRepositoryImpl[domain.UserSession]
|
||||
db *gorm.DB
|
||||
tracer trace.Tracer
|
||||
}
|
||||
@ -21,9 +21,9 @@ type userSessionRepository struct {
|
||||
// NewUserSessionRepository creates a new UserSessionRepository.
|
||||
func NewUserSessionRepository(db *gorm.DB, cfg *config.Config) domain.UserSessionRepository {
|
||||
return &userSessionRepository{
|
||||
BaseRepository: NewBaseRepositoryImpl[domain.UserSession](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user_session.repository"),
|
||||
BaseRepositoryImpl: NewBaseRepositoryImpl[domain.UserSession](db, cfg),
|
||||
db: db,
|
||||
tracer: otel.Tracer("user_session.repository"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ func (r *userSessionRepository) GetByToken(ctx context.Context, token string) (*
|
||||
var session domain.UserSession
|
||||
if err := r.db.WithContext(ctx).Where("token = ?", token).First(&session).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@ -60,4 +60,4 @@ func (r *userSessionRepository) DeleteExpired(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -185,9 +185,9 @@ func (r *workRepository) GetWithAssociationsInTx(ctx context.Context, tx *gorm.D
|
||||
}
|
||||
if err := query.First(&entity, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrEntityNotFound
|
||||
return nil, domain.ErrEntityNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %v", ErrDatabaseOperation, err)
|
||||
return nil, fmt.Errorf("database operation failed: %w", err)
|
||||
}
|
||||
return &entity, nil
|
||||
}
|
||||
|
||||
@ -23,6 +23,13 @@ func (s *WorkRepositoryTestSuite) SetupSuite() {
|
||||
s.WorkRepo = sql.NewWorkRepository(s.DB, cfg)
|
||||
}
|
||||
|
||||
func (s *WorkRepositoryTestSuite) SetupTest() {
|
||||
s.IntegrationTestSuite.SetupTest()
|
||||
s.DB.Exec("DELETE FROM work_copyrights")
|
||||
s.DB.Exec("DELETE FROM copyrights")
|
||||
s.DB.Exec("DELETE FROM works")
|
||||
}
|
||||
|
||||
func (s *WorkRepositoryTestSuite) TestCreateWork() {
|
||||
s.Run("should create a new work with a copyright", func() {
|
||||
// Arrange
|
||||
@ -67,7 +74,7 @@ func (s *WorkRepositoryTestSuite) TestGetWorkByID() {
|
||||
}
|
||||
s.Require().NoError(s.DB.Create(copyright).Error)
|
||||
|
||||
workModel := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
workModel := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
s.Require().NoError(s.DB.Model(workModel).Association("Copyrights").Append(copyright))
|
||||
|
||||
// Act
|
||||
@ -98,7 +105,7 @@ func (s *WorkRepositoryTestSuite) TestUpdateWork() {
|
||||
s.Require().NoError(s.DB.Create(©right1).Error)
|
||||
s.Require().NoError(s.DB.Create(©right2).Error)
|
||||
|
||||
workModel := s.CreateTestWork("Original Title", "en", "Original content")
|
||||
workModel := s.CreateTestWork(s.AdminCtx, "Original Title", "en", "Original content")
|
||||
s.Require().NoError(s.DB.Model(workModel).Association("Copyrights").Append(copyright1))
|
||||
|
||||
workModel.Title = "Updated Title"
|
||||
@ -123,7 +130,7 @@ func (s *WorkRepositoryTestSuite) TestUpdateWork() {
|
||||
func (s *WorkRepositoryTestSuite) TestDeleteWork() {
|
||||
s.Run("should delete an existing work and its associations", func() {
|
||||
// Arrange
|
||||
workModel := s.CreateTestWork("To Be Deleted", "en", "Content")
|
||||
workModel := s.CreateTestWork(s.AdminCtx, "To Be Deleted", "en", "Content")
|
||||
copyright := &domain.Copyright{Name: "C1", Identificator: "C1"}
|
||||
s.Require().NoError(s.DB.Create(copyright).Error)
|
||||
s.Require().NoError(s.DB.Model(workModel).Association("Copyrights").Append(copyright))
|
||||
|
||||
@ -2,17 +2,14 @@ package domain
|
||||
|
||||
import "errors"
|
||||
|
||||
// Common domain-level errors that can be used across repositories and services.
|
||||
var (
|
||||
ErrEntityNotFound = errors.New("entity not found")
|
||||
ErrInvalidID = errors.New("invalid ID: cannot be zero")
|
||||
ErrInvalidInput = errors.New("invalid input parameters")
|
||||
ErrDatabaseOperation = errors.New("database operation failed")
|
||||
ErrContextRequired = errors.New("context is required")
|
||||
ErrTransactionFailed = errors.New("transaction failed")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrValidation = errors.New("validation failed")
|
||||
ErrConflict = errors.New("conflict with existing resource")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrDuplicateEntity = errors.New("duplicate entity")
|
||||
ErrOptimisticLock = errors.New("optimistic lock failure")
|
||||
ErrInvalidOperation = errors.New("invalid operation")
|
||||
ErrConflict = errors.New("conflict")
|
||||
)
|
||||
@ -241,6 +241,7 @@ type BaseRepository[T any] interface {
|
||||
// AuthorRepository defines CRUD methods specific to Author.
|
||||
type AuthorRepository interface {
|
||||
BaseRepository[Author]
|
||||
FindByName(ctx context.Context, name string) (*Author, error)
|
||||
ListByWorkID(ctx context.Context, workID uint) ([]Author, error)
|
||||
ListByBookID(ctx context.Context, bookID uint) ([]Author, error)
|
||||
ListByCountryID(ctx context.Context, countryID uint) ([]Author, error)
|
||||
|
||||
@ -28,7 +28,7 @@ func (s *AnalysisRepositoryTestSuite) SetupTest() {
|
||||
func (s *AnalysisRepositoryTestSuite) TestGetAnalysisData() {
|
||||
s.Run("should return the correct analysis data", func() {
|
||||
// Arrange
|
||||
work := s.CreateTestWork("Test Work", "en", "Test content")
|
||||
work := s.CreateTestWork(s.AdminCtx, "Test Work", "en", "Test content")
|
||||
textMetadata := &domain.TextMetadata{WorkID: work.ID, WordCount: 123}
|
||||
readabilityScore := &domain.ReadabilityScore{WorkID: work.ID, Score: 45.6}
|
||||
languageAnalysis := &domain.LanguageAnalysis{
|
||||
|
||||
@ -48,9 +48,10 @@ func (m *mockSearchClient) IndexWork(ctx context.Context, work *domain.Work, pip
|
||||
// IntegrationTestSuite provides a comprehensive test environment with either in-memory SQLite or mock repositories
|
||||
type IntegrationTestSuite struct {
|
||||
suite.Suite
|
||||
App *app.Application
|
||||
DB *gorm.DB
|
||||
AdminCtx context.Context
|
||||
App *app.Application
|
||||
DB *gorm.DB
|
||||
AdminCtx context.Context
|
||||
AdminToken string
|
||||
}
|
||||
|
||||
// TestConfig holds configuration for the test environment
|
||||
@ -118,7 +119,7 @@ func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) {
|
||||
&domain.Source{}, &domain.Copyright{}, &domain.Monetization{},
|
||||
&domain.WorkStats{}, &domain.Trending{}, &domain.UserSession{}, &domain.Localization{},
|
||||
&domain.LanguageAnalysis{}, &domain.TextMetadata{}, &domain.ReadabilityScore{},
|
||||
&domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{},
|
||||
&domain.TranslationStats{}, &TestEntity{}, &domain.CollectionWork{}, &domain.BookWork{},
|
||||
)
|
||||
s.Require().NoError(err, "Failed to migrate database schema")
|
||||
|
||||
@ -137,7 +138,7 @@ func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) {
|
||||
analyticsService := analytics.NewService(repos.Analytics, analysisRepo, repos.Translation, repos.Work, sentimentProvider)
|
||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||
|
||||
authzService := authz.NewService(repos.Work, repos.Translation)
|
||||
authzService := authz.NewService(repos.Work, repos.Author, repos.User, repos.Translation)
|
||||
authorService := author.NewService(repos.Author)
|
||||
bookService := book.NewService(repos.Book, authzService)
|
||||
bookmarkService := bookmark.NewService(repos.Bookmark, analyticsService)
|
||||
@ -152,7 +153,7 @@ func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) {
|
||||
userService := user.NewService(repos.User, authzService, repos.UserProfile)
|
||||
localizationService := localization.NewService(repos.Localization)
|
||||
authService := app_auth.NewService(repos.User, jwtManager)
|
||||
workService := work.NewService(repos.Work, searchClient, authzService, analyticsService)
|
||||
workService := work.NewService(repos.Work, repos.Author, repos.User, searchClient, authzService, analyticsService)
|
||||
searchService := app_search.NewService(searchClient, localizationService)
|
||||
|
||||
s.App = app.NewApplication(
|
||||
@ -174,21 +175,6 @@ func (s *IntegrationTestSuite) SetupSuite(testConfig *TestConfig) {
|
||||
searchService,
|
||||
analyticsService,
|
||||
)
|
||||
|
||||
// Create a default admin user for tests
|
||||
adminUser := &domain.User{
|
||||
Username: "admin",
|
||||
Email: "admin@test.com",
|
||||
Role: domain.UserRoleAdmin,
|
||||
Active: true,
|
||||
}
|
||||
_ = adminUser.SetPassword("password")
|
||||
err = s.DB.Create(adminUser).Error
|
||||
s.Require().NoError(err)
|
||||
s.AdminCtx = ContextWithClaims(context.Background(), &platform_auth.Claims{
|
||||
UserID: adminUser.ID,
|
||||
Role: string(adminUser.Role),
|
||||
})
|
||||
}
|
||||
|
||||
// TearDownSuite cleans up the test suite
|
||||
@ -213,10 +199,33 @@ func (s *IntegrationTestSuite) SetupTest() {
|
||||
s.DB.Exec("DELETE FROM work_stats")
|
||||
s.DB.Exec("DELETE FROM translation_stats")
|
||||
}
|
||||
|
||||
// Create a default admin user for tests
|
||||
adminUser := &domain.User{
|
||||
Username: "admin",
|
||||
Email: "admin@test.com",
|
||||
Role: domain.UserRoleAdmin,
|
||||
Active: true,
|
||||
}
|
||||
_ = adminUser.SetPassword("password")
|
||||
err := s.DB.Create(adminUser).Error
|
||||
s.Require().NoError(err)
|
||||
s.AdminCtx = ContextWithClaims(context.Background(), &platform_auth.Claims{
|
||||
UserID: adminUser.ID,
|
||||
Role: string(adminUser.Role),
|
||||
})
|
||||
|
||||
// Generate a token for the admin user
|
||||
cfg, err := platform_config.LoadConfig()
|
||||
s.Require().NoError(err)
|
||||
jwtManager := platform_auth.NewJWTManager(cfg)
|
||||
token, err := jwtManager.GenerateToken(adminUser)
|
||||
s.Require().NoError(err)
|
||||
s.AdminToken = token
|
||||
}
|
||||
|
||||
// CreateTestWork creates a test work with optional content
|
||||
func (s *IntegrationTestSuite) CreateTestWork(title, language string, content string) *domain.Work {
|
||||
func (s *IntegrationTestSuite) CreateTestWork(ctx context.Context, title, language string, content string) *domain.Work {
|
||||
work := &domain.Work{
|
||||
Title: title,
|
||||
TranslatableModel: domain.TranslatableModel{
|
||||
@ -225,7 +234,7 @@ func (s *IntegrationTestSuite) CreateTestWork(title, language string, content st
|
||||
}
|
||||
// Note: CreateWork command might not exist or need context. Assuming it does for now.
|
||||
// If CreateWork also requires auth, this context should be s.AdminCtx
|
||||
createdWork, err := s.App.Work.Commands.CreateWork(s.AdminCtx, work)
|
||||
createdWork, err := s.App.Work.Commands.CreateWork(ctx, work)
|
||||
s.Require().NoError(err)
|
||||
|
||||
if content != "" {
|
||||
@ -237,7 +246,7 @@ func (s *IntegrationTestSuite) CreateTestWork(title, language string, content st
|
||||
TranslatableType: "works",
|
||||
IsOriginalLanguage: true, // Assuming the first one is original
|
||||
}
|
||||
_, err = s.App.Translation.Commands.CreateOrUpdateTranslation(s.AdminCtx, translationInput)
|
||||
_, err = s.App.Translation.Commands.CreateOrUpdateTranslation(ctx, translationInput)
|
||||
s.Require().NoError(err)
|
||||
}
|
||||
return createdWork
|
||||
|
||||
@ -2,182 +2,151 @@ package testutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"tercul/internal/domain"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MockUserRepository is a mock implementation of the UserRepository interface.
|
||||
// MockUserRepository is a mock implementation of the UserRepository interface using testify/mock.
|
||||
type MockUserRepository struct {
|
||||
Users []*domain.User
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// NewMockUserRepository creates a new MockUserRepository.
|
||||
func NewMockUserRepository() *MockUserRepository {
|
||||
return &MockUserRepository{Users: []*domain.User{}}
|
||||
}
|
||||
|
||||
// Create adds a new user to the mock repository.
|
||||
func (m *MockUserRepository) Create(ctx context.Context, user *domain.User) error {
|
||||
user.ID = uint(len(m.Users) + 1)
|
||||
m.Users = append(m.Users, user)
|
||||
return nil
|
||||
args := m.Called(ctx, user)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
// GetByID retrieves a user by their ID from the mock repository.
|
||||
func (m *MockUserRepository) GetByID(ctx context.Context, id uint) (*domain.User, error) {
|
||||
for _, u := range m.Users {
|
||||
if u.ID == id {
|
||||
return u, nil
|
||||
}
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
// FindByUsername retrieves a user by their username from the mock repository.
|
||||
func (m *MockUserRepository) FindByUsername(ctx context.Context, username string) (*domain.User, error) {
|
||||
for _, u := range m.Users {
|
||||
if strings.EqualFold(u.Username, username) {
|
||||
return u, nil
|
||||
}
|
||||
args := m.Called(ctx, username)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
// FindByEmail retrieves a user by their email from the mock repository.
|
||||
func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
for _, u := range m.Users {
|
||||
if strings.EqualFold(u.Email, email) {
|
||||
return u, nil
|
||||
}
|
||||
args := m.Called(ctx, email)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
// ListByRole retrieves users by their role from the mock repository.
|
||||
func (m *MockUserRepository) ListByRole(ctx context.Context, role domain.UserRole) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
for _, u := range m.Users {
|
||||
if u.Role == role {
|
||||
users = append(users, *u)
|
||||
}
|
||||
args := m.Called(ctx, role)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return users, nil
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
// The rest of the BaseRepository methods can be stubbed out or implemented as needed.
|
||||
func (m *MockUserRepository) CreateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
return m.Create(ctx, entity)
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetByIDWithOptions(ctx context.Context, id uint, options *domain.QueryOptions) (*domain.User, error) {
|
||||
return m.GetByID(ctx, id)
|
||||
args := m.Called(ctx, id, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) Update(ctx context.Context, entity *domain.User) error {
|
||||
for i, u := range m.Users {
|
||||
if u.ID == entity.ID {
|
||||
m.Users[i] = entity
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return gorm.ErrRecordNotFound
|
||||
args := m.Called(ctx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) UpdateInTx(ctx context.Context, tx *gorm.DB, entity *domain.User) error {
|
||||
return m.Update(ctx, entity)
|
||||
args := m.Called(ctx, tx, entity)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) Delete(ctx context.Context, id uint) error {
|
||||
for i, u := range m.Users {
|
||||
if u.ID == id {
|
||||
m.Users = append(m.Users[:i], m.Users[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return gorm.ErrRecordNotFound
|
||||
args := m.Called(ctx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) DeleteInTx(ctx context.Context, tx *gorm.DB, id uint) error {
|
||||
return m.Delete(ctx, id)
|
||||
args := m.Called(ctx, tx, id)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) List(ctx context.Context, page, pageSize int) (*domain.PaginatedResult[domain.User], error) {
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if start > len(m.Users) {
|
||||
start = len(m.Users)
|
||||
args := m.Called(ctx, page, pageSize)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
if end > len(m.Users) {
|
||||
end = len(m.Users)
|
||||
}
|
||||
|
||||
paginatedUsers := m.Users[start:end]
|
||||
var users []domain.User
|
||||
for _, u := range paginatedUsers {
|
||||
users = append(users, *u)
|
||||
}
|
||||
|
||||
totalCount := int64(len(m.Users))
|
||||
totalPages := int(totalCount) / pageSize
|
||||
if int(totalCount)%pageSize != 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return &domain.PaginatedResult[domain.User]{
|
||||
Items: users,
|
||||
TotalCount: totalCount,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
HasNext: page < totalPages,
|
||||
HasPrev: page > 1,
|
||||
}, nil
|
||||
return args.Get(0).(*domain.PaginatedResult[domain.User]), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) ListWithOptions(ctx context.Context, options *domain.QueryOptions) ([]domain.User, error) {
|
||||
// This is a mock implementation and doesn't handle options.
|
||||
return m.ListAll(ctx)
|
||||
args := m.Called(ctx, options)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) ListAll(ctx context.Context) ([]domain.User, error) {
|
||||
var users []domain.User
|
||||
for _, u := range m.Users {
|
||||
users = append(users, *u)
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return users, nil
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) Count(ctx context.Context) (int64, error) {
|
||||
return int64(len(m.Users)), nil
|
||||
args := m.Called(ctx)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) CountWithOptions(ctx context.Context, options *domain.QueryOptions) (int64, error) {
|
||||
// This is a mock implementation and doesn't handle options.
|
||||
return m.Count(ctx)
|
||||
args := m.Called(ctx, options)
|
||||
return args.Get(0).(int64), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) FindWithPreload(ctx context.Context, preloads []string, id uint) (*domain.User, error) {
|
||||
return m.GetByID(ctx, id)
|
||||
args := m.Called(ctx, preloads, id)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) GetAllForSync(ctx context.Context, batchSize, offset int) ([]domain.User, error) {
|
||||
start := offset
|
||||
end := start + batchSize
|
||||
if start > len(m.Users) {
|
||||
return []domain.User{}, nil
|
||||
args := m.Called(ctx, batchSize, offset)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
if end > len(m.Users) {
|
||||
end = len(m.Users)
|
||||
}
|
||||
var users []domain.User
|
||||
for _, u := range m.Users[start:end] {
|
||||
users = append(users, *u)
|
||||
}
|
||||
return users, nil
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) Exists(ctx context.Context, id uint) (bool, error) {
|
||||
_, err := m.GetByID(ctx, id)
|
||||
return err == nil, nil
|
||||
args := m.Called(ctx, id)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) BeginTx(ctx context.Context) (*gorm.DB, error) {
|
||||
return nil, nil
|
||||
args := m.Called(ctx)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).(*gorm.DB), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) WithTx(ctx context.Context, fn func(tx *gorm.DB) error) error {
|
||||
return fn(nil)
|
||||
args := m.Called(ctx, fn)
|
||||
return args.Error(0)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user