package user import ( "context" "errors" "testing" "tercul/internal/app/authz" "tercul/internal/domain" platform_auth "tercul/internal/platform/auth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) type UserCommandsSuite struct { suite.Suite repo *mockUserRepository authzSvc *authz.Service commands *UserCommands } func (s *UserCommandsSuite) SetupTest() { 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) } func TestUserCommandsSuite(t *testing.T) { suite.Run(t, new(UserCommandsSuite)) } func (s *UserCommandsSuite) TestUpdateUser_Success_Self() { // Arrange ctx := platform_auth.ContextWithUserID(context.Background(), 1) input := UpdateUserInput{ID: 1, Username: strPtr("new_username")} 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) // Assert 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() { // Arrange ctx := platform_auth.ContextWithAdminUser(context.Background(), 99) // Admin user input := UpdateUserInput{ID: 1, Username: strPtr("new_username_by_admin")} 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) // Assert 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() { // Arrange ctx := platform_auth.ContextWithUserID(context.Background(), 2) // Different user input := UpdateUserInput{ID: 1, Username: strPtr("forbidden_username")} // No need to mock GetByID, as the auth check happens first. // Act _, err := s.commands.UpdateUser(ctx, input) // 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() { // Arrange ctx := context.Background() // No user in context input := UpdateUserInput{ID: 1, Username: strPtr("unauthorized_username")} // Act _, err := s.commands.UpdateUser(ctx, input) // 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()) }