package graphql import ( "context" "fmt" "tercul/internal/adapters/graphql/model" "tercul/internal/app" "tercul/internal/app/authz" "tercul/internal/app/user" "tercul/internal/domain" platform_auth "tercul/internal/platform/auth" "testing" "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 uuid.UUID) (*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 uuid.UUID) (*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 uuid.UUID) (*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) }) }