diff --git a/integration_testing/v2_authorizations_test.go b/integration_testing/v2_authorizations_test.go new file mode 100644 index 0000000..d34546c --- /dev/null +++ b/integration_testing/v2_authorizations_test.go @@ -0,0 +1,482 @@ +package integration_testing_test + +import ( + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/boxboxjason/sonarqube-client-go/integration_testing/helpers" + "github.com/boxboxjason/sonarqube-client-go/sonar" +) + +var _ = Describe("V2 Authorizations Service", Ordered, func() { + var ( + client *sonar.Client + cleanup *helpers.CleanupManager + ) + + BeforeAll(func() { + var err error + client, err = helpers.NewDefaultClient() + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + cleanup = helpers.NewCleanupManager(client) + }) + + AfterAll(func() { + errors := cleanup.Cleanup() + for _, err := range errors { + GinkgoWriter.Printf("Cleanup error: %v\n", err) + } + }) + + // ========================================================================= + // Groups + // ========================================================================= + Describe("SearchGroups", func() { + Context("without options", func() { + It("should return a list of groups", func() { + result, resp, err := client.V2.Authorizations.SearchGroups(nil) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(result).NotTo(BeNil()) + Expect(result.Groups).NotTo(BeEmpty()) + Expect(result.Page.PageSize).To(BeNumerically(">", 0)) + }) + }) + + Context("with query filter", func() { + It("should filter groups by query", func() { + groupName := helpers.UniqueResourceName("v2srchg") + group, resp, err := client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Name: groupName, + Description: "Search test group", + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + cleanup.RegisterCleanup("v2-group-search", group.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroup(group.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + + result, resp, err := client.V2.Authorizations.SearchGroups(&sonar.AuthorizationsSearchGroupsOptions{ + Query: groupName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(result).NotTo(BeNil()) + found := false + for _, g := range result.Groups { + if g.Name == groupName { + found = true + break + } + } + Expect(found).To(BeTrue()) + }) + }) + + Context("with pagination", func() { + It("should respect page size", func() { + result, resp, err := client.V2.Authorizations.SearchGroups(&sonar.AuthorizationsSearchGroupsOptions{ + PaginationParamsV2: sonar.PaginationParamsV2{ + PageSize: 1, + }, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(result).NotTo(BeNil()) + Expect(len(result.Groups)).To(BeNumerically("<=", 1)) + Expect(result.Page.PageSize).To(BeNumerically("==", 1)) + }) + }) + }) + + Describe("CreateGroup", func() { + Context("with valid parameters", func() { + It("should create a group", func() { + groupName := helpers.UniqueResourceName("v2grp") + group, resp, err := client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Name: groupName, + Description: "V2 integration test group", + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + Expect(group).NotTo(BeNil()) + Expect(group.Name).To(Equal(groupName)) + Expect(group.Description).To(Equal("V2 integration test group")) + Expect(group.Id).NotTo(BeEmpty()) + + cleanup.RegisterCleanup("v2-group-create", group.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroup(group.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + }) + + It("should create a group with name only", func() { + groupName := helpers.UniqueResourceName("v2grpmin") + group, resp, err := client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Name: groupName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + Expect(group).NotTo(BeNil()) + Expect(group.Name).To(Equal(groupName)) + Expect(group.Id).NotTo(BeEmpty()) + + cleanup.RegisterCleanup("v2-group-min", group.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroup(group.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + }) + }) + + Context("parameter validation", func() { + It("should fail with nil request", func() { + group, resp, err := client.V2.Authorizations.CreateGroup(nil) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(group).To(BeNil()) + }) + + It("should fail with empty name", func() { + group, resp, err := client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Description: "No Name Group", + }) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(group).To(BeNil()) + }) + }) + }) + + Describe("FetchGroup", func() { + var createdGroup *sonar.Group + + BeforeAll(func() { + groupName := helpers.UniqueResourceName("v2gfetch") + var resp *http.Response + var err error + createdGroup, resp, err = client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Name: groupName, + Description: "Fetch test group", + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + cleanup.RegisterCleanup("v2-group-fetch", createdGroup.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroup(createdGroup.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + }) + + Context("with valid group ID", func() { + It("should fetch the group by ID", func() { + fetched, resp, err := client.V2.Authorizations.FetchGroup(createdGroup.Id) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(fetched).NotTo(BeNil()) + Expect(fetched.Id).To(Equal(createdGroup.Id)) + Expect(fetched.Name).To(Equal(createdGroup.Name)) + Expect(fetched.Description).To(Equal("Fetch test group")) + }) + }) + + Context("parameter validation", func() { + It("should fail with empty group ID", func() { + group, resp, err := client.V2.Authorizations.FetchGroup("") + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(group).To(BeNil()) + }) + }) + }) + + Describe("UpdateGroup", func() { + var createdGroup *sonar.Group + + BeforeAll(func() { + groupName := helpers.UniqueResourceName("v2gupd") + var resp *http.Response + var err error + createdGroup, resp, err = client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Name: groupName, + Description: "Original description", + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + cleanup.RegisterCleanup("v2-group-update", createdGroup.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroup(createdGroup.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + }) + + Context("with valid parameters", func() { + It("should update group name", func() { + newName := helpers.UniqueResourceName("v2gnew") + updated, resp, err := client.V2.Authorizations.UpdateGroup(createdGroup.Id, &sonar.AuthorizationsUpdateGroupOptions{ + Name: newName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(updated).NotTo(BeNil()) + Expect(updated.Name).To(Equal(newName)) + }) + + It("should update group description", func() { + description := "Updated description" + updated, resp, err := client.V2.Authorizations.UpdateGroup(createdGroup.Id, &sonar.AuthorizationsUpdateGroupOptions{ + Description: &description, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(updated).NotTo(BeNil()) + Expect(updated.Description).To(Equal("Updated description")) + }) + }) + + Context("parameter validation", func() { + It("should fail with empty group ID", func() { + group, resp, err := client.V2.Authorizations.UpdateGroup("", &sonar.AuthorizationsUpdateGroupOptions{ + Name: "Updated", + }) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(group).To(BeNil()) + }) + + It("should fail with nil request", func() { + group, resp, err := client.V2.Authorizations.UpdateGroup(createdGroup.Id, nil) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(group).To(BeNil()) + }) + }) + }) + + Describe("DeleteGroup", func() { + Context("with valid group ID", func() { + It("should delete a group", func() { + groupName := helpers.UniqueResourceName("v2gdel") + group, resp, err := client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Name: groupName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + cleanup.RegisterCleanup("v2-group-delete", group.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroup(group.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + + resp, err = client.V2.Authorizations.DeleteGroup(group.Id) + Expect(err).NotTo(HaveOccurred()) + Expect(resp).NotTo(BeNil()) + Expect(resp.StatusCode).To(BeNumerically(">=", 200)) + Expect(resp.StatusCode).To(BeNumerically("<", 300)) + }) + }) + + Context("parameter validation", func() { + It("should fail with empty group ID", func() { + resp, err := client.V2.Authorizations.DeleteGroup("") + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + }) + }) + }) + + // ========================================================================= + // Group Memberships + // ========================================================================= + Describe("SearchGroupMemberships", func() { + Context("without options", func() { + It("should return a list of group memberships", func() { + result, resp, err := client.V2.Authorizations.SearchGroupMemberships(nil) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(result).NotTo(BeNil()) + Expect(result.GroupMemberships).NotTo(BeEmpty()) + }) + }) + + Context("with group filter", func() { + It("should filter memberships by group ID", func() { + groups, resp, err := client.V2.Authorizations.SearchGroups(nil) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(groups.Groups).NotTo(BeEmpty()) + + groupID := groups.Groups[0].Id + result, resp, err := client.V2.Authorizations.SearchGroupMemberships(&sonar.AuthorizationsSearchGroupMembershipsOptions{ + GroupId: groupID, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(result).NotTo(BeNil()) + for _, m := range result.GroupMemberships { + Expect(m.GroupId).To(Equal(groupID)) + } + }) + }) + }) + + Describe("CreateGroupMembership", func() { + var ( + createdGroup *sonar.Group + createdUser *sonar.UserV2 + ) + + BeforeAll(func() { + groupName := helpers.UniqueResourceName("v2gmem") + var resp *http.Response + var err error + createdGroup, resp, err = client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Name: groupName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + cleanup.RegisterCleanup("v2-group-membership", createdGroup.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroup(createdGroup.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + + login := helpers.UniqueResourceName("v2umem") + createdUser, resp, err = client.V2.UsersManagement.Create(&sonar.UsersCreateOptionsV2{ + Login: login, + Name: "V2 Membership User", + Password: "testPassword123!", + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + + cleanup.RegisterCleanup("v2-user-membership", createdUser.Id, func() error { + _, cleanupErr := client.V2.UsersManagement.Deactivate(&sonar.UsersDeactivateOptionsV2{ + Id: createdUser.Id, + Anonymize: true, + }) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + }) + + Context("with valid parameters", func() { + It("should create a group membership", func() { + membership, resp, err := client.V2.Authorizations.CreateGroupMembership(&sonar.AuthorizationsCreateGroupMembershipOptions{ + GroupId: createdGroup.Id, + UserId: createdUser.Id, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + Expect(membership).NotTo(BeNil()) + Expect(membership.GroupId).To(Equal(createdGroup.Id)) + Expect(membership.UserId).To(Equal(createdUser.Id)) + Expect(membership.Id).NotTo(BeEmpty()) + + cleanup.RegisterCleanup("v2-membership", membership.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroupMembership(membership.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + + result, resp, err := client.V2.Authorizations.SearchGroupMemberships(&sonar.AuthorizationsSearchGroupMembershipsOptions{ + GroupId: createdGroup.Id, + UserId: createdUser.Id, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(result.GroupMemberships).NotTo(BeEmpty()) + }) + }) + + Context("parameter validation", func() { + It("should fail with nil request", func() { + membership, resp, err := client.V2.Authorizations.CreateGroupMembership(nil) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(membership).To(BeNil()) + }) + + It("should fail with empty group ID", func() { + membership, resp, err := client.V2.Authorizations.CreateGroupMembership(&sonar.AuthorizationsCreateGroupMembershipOptions{ + UserId: createdUser.Id, + }) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(membership).To(BeNil()) + }) + + It("should fail with empty user ID", func() { + membership, resp, err := client.V2.Authorizations.CreateGroupMembership(&sonar.AuthorizationsCreateGroupMembershipOptions{ + GroupId: createdGroup.Id, + }) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(membership).To(BeNil()) + }) + }) + }) + + Describe("DeleteGroupMembership", func() { + Context("with valid membership ID", func() { + It("should delete a group membership", func() { + groupName := helpers.UniqueResourceName("v2gmdel") + group, resp, err := client.V2.Authorizations.CreateGroup(&sonar.AuthorizationsCreateGroupOptions{ + Name: groupName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + cleanup.RegisterCleanup("v2-group-delm", group.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroup(group.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + + login := helpers.UniqueResourceName("v2umdel") + user, resp, err := client.V2.UsersManagement.Create(&sonar.UsersCreateOptionsV2{ + Login: login, + Name: "V2 Del Membership User", + Password: "testPassword123!", + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + + cleanup.RegisterCleanup("v2-user-delm", user.Id, func() error { + _, cleanupErr := client.V2.UsersManagement.Deactivate(&sonar.UsersDeactivateOptionsV2{ + Id: user.Id, + Anonymize: true, + }) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + + membership, resp, err := client.V2.Authorizations.CreateGroupMembership(&sonar.AuthorizationsCreateGroupMembershipOptions{ + GroupId: group.Id, + UserId: user.Id, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + cleanup.RegisterCleanup("v2-membership-del", membership.Id, func() error { + _, cleanupErr := client.V2.Authorizations.DeleteGroupMembership(membership.Id) + return helpers.IgnoreNotFoundError(cleanupErr) + }) + + resp, err = client.V2.Authorizations.DeleteGroupMembership(membership.Id) + Expect(err).NotTo(HaveOccurred()) + Expect(resp).NotTo(BeNil()) + Expect(resp.StatusCode).To(BeNumerically(">=", 200)) + Expect(resp.StatusCode).To(BeNumerically("<", 300)) + }) + }) + + Context("parameter validation", func() { + It("should fail with empty membership ID", func() { + resp, err := client.V2.Authorizations.DeleteGroupMembership("") + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + }) + }) + }) +}) diff --git a/sonar/authorizations_v2_service.go b/sonar/authorizations_v2_service.go index 65115e8..ec7e333 100644 --- a/sonar/authorizations_v2_service.go +++ b/sonar/authorizations_v2_service.go @@ -1,8 +1,398 @@ package sonar +import ( + "fmt" + "net/http" +) + // AuthorizationsService handles communication with the Authorizations related // methods of the SonarQube V2 API. type AuthorizationsService struct { // client is used to communicate with the SonarQube API. client *Client } + +// ----------------------------------------------------------------------------- +// Shared Types +// ----------------------------------------------------------------------------- + +// Group represents a group returned by V2 API endpoints. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type Group struct { + // Default indicates whether this is a default group. + Default bool `json:"default,omitempty"` + // Description is the group description. + Description string `json:"description,omitempty"` + // Id is the group's unique identifier. + Id string `json:"id,omitempty"` + // Managed indicates whether the group is managed externally. + Managed bool `json:"managed,omitempty"` + // Name is the group name. + Name string `json:"name,omitempty"` +} + +// GroupMembership represents a group membership returned by V2 API endpoints. +type GroupMembership struct { + // GroupId is the group's unique identifier. + GroupId string `json:"groupId,omitempty"` + // Id is the membership's unique identifier. + Id string `json:"id,omitempty"` + // UserId is the user's unique identifier. + UserId string `json:"userId,omitempty"` +} + +// ----------------------------------------------------------------------------- +// Response Types +// ----------------------------------------------------------------------------- + +// AuthorizationsGroupsSearch represents the response from searching groups. +type AuthorizationsGroupsSearch struct { + // Groups is the list of groups. + Groups []Group `json:"groups,omitempty"` + // Page contains pagination information. + Page PageResponseV2 `json:"page,omitzero"` +} + +// AuthorizationsGroupMembershipsSearch represents the response from searching group memberships. +type AuthorizationsGroupMembershipsSearch struct { + // GroupMemberships is the list of group memberships. + GroupMemberships []GroupMembership `json:"groupMemberships,omitempty"` + // Page contains pagination information. + Page PageResponseV2 `json:"page,omitzero"` +} + +// ----------------------------------------------------------------------------- +// Option Types (Query Parameters) +// ----------------------------------------------------------------------------- + +// AuthorizationsSearchGroupsOptions contains query parameters for the SearchGroups method. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type AuthorizationsSearchGroupsOptions struct { + PaginationParamsV2 + + // Managed filters managed or non-managed groups. + Managed *bool `json:"managed,omitempty"` + // Query filters on group name (partial match, case insensitive). + Query string `json:"q,omitempty"` + // UserId filters groups containing the user. Only available for system administrators. + // Internal: this parameter is marked as internal in the SonarQube API. + UserId string `json:"userId,omitempty"` +} + +// AuthorizationsSearchGroupMembershipsOptions contains query parameters for the SearchGroupMemberships method. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type AuthorizationsSearchGroupMembershipsOptions struct { + PaginationParamsV2 + + // GroupId filters memberships by group ID. + GroupId string `json:"groupId,omitempty"` + // UserId filters memberships by user ID. + UserId string `json:"userId,omitempty"` +} + +// ----------------------------------------------------------------------------- +// Request Types (JSON Body) +// ----------------------------------------------------------------------------- + +// AuthorizationsCreateGroupOptions contains parameters for creating a group. +type AuthorizationsCreateGroupOptions struct { + // Description is the group description. Maximum 200 characters. + Description string `json:"description,omitempty"` + // Name is the group name. Must be unique. The value 'anyone' is reserved. + // This field is required. Must be between 1 and 255 characters. + Name string `json:"name"` +} + +// AuthorizationsUpdateGroupOptions contains parameters for updating a group. +// All fields are optional (PATCH merge semantics). +type AuthorizationsUpdateGroupOptions struct { + // Description is the group description. Maximum 200 characters. + // Use nil to leave unchanged, or a pointer to an empty string to clear it. + Description *string `json:"description,omitempty"` + // Name is the group name. Must be between 1 and 255 characters. + Name string `json:"name,omitempty"` +} + +// AuthorizationsCreateGroupMembershipOptions contains parameters for creating a group membership. +type AuthorizationsCreateGroupMembershipOptions struct { + // GroupId is the ID of the group. + GroupId string `json:"groupId,omitempty"` + // UserId is the ID of the user. + UserId string `json:"userId,omitempty"` +} + +// ----------------------------------------------------------------------------- +// Validation +// ----------------------------------------------------------------------------- + +// ValidateSearchGroupsOpt validates the AuthorizationsSearchGroupsOptions. +func (s *AuthorizationsService) ValidateSearchGroupsOpt(opt *AuthorizationsSearchGroupsOptions) error { + if opt == nil { + return nil + } + + return opt.Validate() +} + +// ValidateSearchGroupMembershipsOpt validates the AuthorizationsSearchGroupMembershipsOptions. +func (s *AuthorizationsService) ValidateSearchGroupMembershipsOpt(opt *AuthorizationsSearchGroupMembershipsOptions) error { + if opt == nil { + return nil + } + + return opt.Validate() +} + +// ValidateCreateGroupRequest validates the AuthorizationsCreateGroupOptions. +func (s *AuthorizationsService) ValidateCreateGroupRequest(opt *AuthorizationsCreateGroupOptions) error { + if opt == nil { + return NewValidationError("opt", "must not be nil", ErrMissingRequired) + } + + err := ValidateRequired(opt.Name, "Name") + if err != nil { + return err + } + + err = ValidateMaxLength(opt.Name, MaxGroupNameLength, "Name") + if err != nil { + return err + } + + err = ValidateMaxLength(opt.Description, MaxGroupDescriptionLength, "Description") + if err != nil { + return err + } + + return nil +} + +// ValidateUpdateGroupRequest validates the AuthorizationsUpdateGroupOptions. +func (s *AuthorizationsService) ValidateUpdateGroupRequest(groupID string, opt *AuthorizationsUpdateGroupOptions) error { + err := ValidateRequired(groupID, "Id") + if err != nil { + return err + } + + if opt == nil { + return NewValidationError("opt", "must not be nil", ErrMissingRequired) + } + + err = ValidateMinLength(opt.Name, 1, "Name") + if err != nil { + return err + } + + err = ValidateMaxLength(opt.Name, MaxGroupNameLength, "Name") + if err != nil { + return err + } + + if opt.Description != nil { + err = ValidateMaxLength(*opt.Description, MaxGroupDescriptionLength, "Description") + if err != nil { + return err + } + } + + return nil +} + +// ValidateCreateGroupMembershipRequest validates the AuthorizationsCreateGroupMembershipOptions. +func (s *AuthorizationsService) ValidateCreateGroupMembershipRequest(opt *AuthorizationsCreateGroupMembershipOptions) error { + if opt == nil { + return NewValidationError("opt", "must not be nil", ErrMissingRequired) + } + + err := ValidateRequired(opt.GroupId, "GroupId") + if err != nil { + return err + } + + err = ValidateRequired(opt.UserId, "UserId") + if err != nil { + return err + } + + return nil +} + +// ----------------------------------------------------------------------------- +// Service Methods +// ----------------------------------------------------------------------------- + +// SearchGroups returns a list of groups matching the search criteria. +// The results are sorted alphabetically by group name. +func (s *AuthorizationsService) SearchGroups(opt *AuthorizationsSearchGroupsOptions) (*AuthorizationsGroupsSearch, *http.Response, error) { + err := s.ValidateSearchGroupsOpt(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(http.MethodGet, "authorizations/groups", opt, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(AuthorizationsGroupsSearch) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// CreateGroup creates a new group. +func (s *AuthorizationsService) CreateGroup(opt *AuthorizationsCreateGroupOptions) (*Group, *http.Response, error) { + err := s.ValidateCreateGroupRequest(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(http.MethodPost, "authorizations/groups", nil, opt) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(Group) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// FetchGroup retrieves a single group by ID. +func (s *AuthorizationsService) FetchGroup(groupID string) (*Group, *http.Response, error) { + err := ValidateRequired(groupID, "Id") + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(http.MethodGet, "authorizations/groups/"+groupID, nil, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(Group) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// DeleteGroup deletes a group by ID. +func (s *AuthorizationsService) DeleteGroup(groupID string) (*http.Response, error) { + err := ValidateRequired(groupID, "Id") + if err != nil { + return nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(http.MethodDelete, "authorizations/groups/"+groupID, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// UpdateGroup updates a group's name or description. +func (s *AuthorizationsService) UpdateGroup(groupID string, opt *AuthorizationsUpdateGroupOptions) (*Group, *http.Response, error) { + err := s.ValidateUpdateGroupRequest(groupID, opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(http.MethodPatch, "authorizations/groups/"+groupID, nil, opt) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(Group) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// SearchGroupMemberships returns a list of group memberships matching the search criteria. +func (s *AuthorizationsService) SearchGroupMemberships(opt *AuthorizationsSearchGroupMembershipsOptions) (*AuthorizationsGroupMembershipsSearch, *http.Response, error) { + err := s.ValidateSearchGroupMembershipsOpt(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(http.MethodGet, "authorizations/group-memberships", opt, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(AuthorizationsGroupMembershipsSearch) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// CreateGroupMembership adds a user to a group. +func (s *AuthorizationsService) CreateGroupMembership(opt *AuthorizationsCreateGroupMembershipOptions) (*GroupMembership, *http.Response, error) { + err := s.ValidateCreateGroupMembershipRequest(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(http.MethodPost, "authorizations/group-memberships", nil, opt) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(GroupMembership) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// DeleteGroupMembership removes a user from a group. +func (s *AuthorizationsService) DeleteGroupMembership(membershipID string) (*http.Response, error) { + err := ValidateRequired(membershipID, "Id") + if err != nil { + return nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(http.MethodDelete, "authorizations/group-memberships/"+membershipID, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + resp, err := s.client.Do(req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} diff --git a/sonar/authorizations_v2_service_test.go b/sonar/authorizations_v2_service_test.go new file mode 100644 index 0000000..705e575 --- /dev/null +++ b/sonar/authorizations_v2_service_test.go @@ -0,0 +1,336 @@ +package sonar + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func stringPtr(v string) *string { + return &v +} + +// ============================================================================= +// SearchGroups +// ============================================================================= + +func TestAuthorizationsV2_SearchGroups(t *testing.T) { + response := AuthorizationsGroupsSearch{ + Groups: []Group{ + {Id: "g1", Name: "admins", Description: "Administrator group", Managed: false}, + {Id: "g2", Name: "members", Default: true}, + }, + Page: PageResponseV2{PageIndex: 1, PageSize: 50, Total: 2}, + } + server := newTestServer(t, mockHandler(t, http.MethodGet, "/v2/authorizations/groups", http.StatusOK, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.Authorizations.SearchGroups(nil) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Len(t, result.Groups, 2) + assert.Equal(t, "admins", result.Groups[0].Name) +} + +func TestAuthorizationsV2_SearchGroups_WithOptions(t *testing.T) { + managed := true + response := AuthorizationsGroupsSearch{ + Groups: []Group{{Id: "g1", Name: "admins", Managed: true}}, + Page: PageResponseV2{PageIndex: 2, PageSize: 10, Total: 1}, + } + server := newTestServer(t, mockHandlerWithParams(t, http.MethodGet, "/v2/authorizations/groups", http.StatusOK, + map[string]string{ + "q": "admins", + "managed": "true", + "pageSize": "10", + "pageIndex": "2", + "userId": "u1", + }, + response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.Authorizations.SearchGroups(&AuthorizationsSearchGroupsOptions{ + PaginationParamsV2: PaginationParamsV2{PageIndex: 2, PageSize: 10}, + Managed: &managed, + Query: "admins", + UserId: "u1", + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Len(t, result.Groups, 1) +} + +func TestAuthorizationsV2_SearchGroups_Validation(t *testing.T) { + client := newLocalhostClient(t) + + _, _, err := client.V2.Authorizations.SearchGroups(&AuthorizationsSearchGroupsOptions{ + PaginationParamsV2: PaginationParamsV2{PageSize: 600}, + }) + assert.Error(t, err) +} + +// ============================================================================= +// CreateGroup +// ============================================================================= + +func TestAuthorizationsV2_CreateGroup(t *testing.T) { + response := Group{ + Id: "g-new", + Name: "new-group", + Description: "A new group", + } + server := newTestServer(t, mockJSONBodyHandler(t, http.MethodPost, "/v2/authorizations/groups", http.StatusCreated, + &AuthorizationsCreateGroupOptions{ + Name: "new-group", + Description: "A new group", + }, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.Authorizations.CreateGroup(&AuthorizationsCreateGroupOptions{ + Name: "new-group", + Description: "A new group", + }) + require.NoError(t, err) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + assert.Equal(t, "new-group", result.Name) +} + +func TestAuthorizationsV2_CreateGroup_Validation(t *testing.T) { + client := newLocalhostClient(t) + + tests := []struct { + name string + opt *AuthorizationsCreateGroupOptions + }{ + {"nil opt", nil}, + {"missing name", &AuthorizationsCreateGroupOptions{}}, + {"name too long", &AuthorizationsCreateGroupOptions{Name: strings.Repeat("a", MaxGroupNameLength+1)}}, + {"description too long", &AuthorizationsCreateGroupOptions{Name: "ok", Description: strings.Repeat("a", MaxGroupDescriptionLength+1)}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err := client.V2.Authorizations.CreateGroup(tt.opt) + assert.Error(t, err) + }) + } +} + +// ============================================================================= +// FetchGroup +// ============================================================================= + +func TestAuthorizationsV2_FetchGroup(t *testing.T) { + response := Group{ + Id: "g1", + Name: "admins", + } + server := newTestServer(t, mockHandler(t, http.MethodGet, "/v2/authorizations/groups/g1", http.StatusOK, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.Authorizations.FetchGroup("g1") + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "admins", result.Name) +} + +func TestAuthorizationsV2_FetchGroup_Validation(t *testing.T) { + client := newLocalhostClient(t) + + _, _, err := client.V2.Authorizations.FetchGroup("") + assert.Error(t, err) +} + +// ============================================================================= +// DeleteGroup +// ============================================================================= + +func TestAuthorizationsV2_DeleteGroup(t *testing.T) { + server := newTestServer(t, mockEmptyHandler(t, http.MethodDelete, "/v2/authorizations/groups/g1", http.StatusNoContent)) + client := newTestClient(t, server.url()) + + resp, err := client.V2.Authorizations.DeleteGroup("g1") + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) +} + +func TestAuthorizationsV2_DeleteGroup_Validation(t *testing.T) { + client := newLocalhostClient(t) + + _, err := client.V2.Authorizations.DeleteGroup("") + assert.Error(t, err) +} + +// ============================================================================= +// UpdateGroup +// ============================================================================= + +func TestAuthorizationsV2_UpdateGroup(t *testing.T) { + response := Group{ + Id: "g1", + Name: "renamed-group", + Description: "Updated description", + } + server := newTestServer(t, mockPatchHandler(t, "/v2/authorizations/groups/g1", http.StatusOK, + &AuthorizationsUpdateGroupOptions{ + Name: "renamed-group", + Description: stringPtr("Updated description"), + }, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.Authorizations.UpdateGroup("g1", &AuthorizationsUpdateGroupOptions{ + Name: "renamed-group", + Description: stringPtr("Updated description"), + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "renamed-group", result.Name) +} + +func TestAuthorizationsV2_UpdateGroup_Validation(t *testing.T) { + client := newLocalhostClient(t) + + tests := []struct { + name string + id string + opt *AuthorizationsUpdateGroupOptions + }{ + {"missing id", "", &AuthorizationsUpdateGroupOptions{Name: "ok"}}, + {"nil opt", "g1", nil}, + {"name too long", "g1", &AuthorizationsUpdateGroupOptions{Name: strings.Repeat("a", MaxGroupNameLength+1)}}, + {"description too long", "g1", &AuthorizationsUpdateGroupOptions{Description: stringPtr(strings.Repeat("a", MaxGroupDescriptionLength+1))}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err := client.V2.Authorizations.UpdateGroup(tt.id, tt.opt) + assert.Error(t, err) + }) + } +} + +// ============================================================================= +// SearchGroupMemberships +// ============================================================================= + +func TestAuthorizationsV2_SearchGroupMemberships(t *testing.T) { + response := AuthorizationsGroupMembershipsSearch{ + GroupMemberships: []GroupMembership{ + {Id: "m1", GroupId: "g1", UserId: "u1"}, + }, + Page: PageResponseV2{PageIndex: 1, PageSize: 50, Total: 1}, + } + server := newTestServer(t, mockHandler(t, http.MethodGet, "/v2/authorizations/group-memberships", http.StatusOK, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.Authorizations.SearchGroupMemberships(nil) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Len(t, result.GroupMemberships, 1) + assert.Equal(t, "g1", result.GroupMemberships[0].GroupId) +} + +func TestAuthorizationsV2_SearchGroupMemberships_WithOptions(t *testing.T) { + response := AuthorizationsGroupMembershipsSearch{ + GroupMemberships: []GroupMembership{{Id: "m1", GroupId: "g1", UserId: "u1"}}, + Page: PageResponseV2{PageIndex: 2, PageSize: 10, Total: 1}, + } + server := newTestServer(t, mockHandlerWithParams(t, http.MethodGet, "/v2/authorizations/group-memberships", http.StatusOK, + map[string]string{ + "groupId": "g1", + "userId": "u1", + "pageSize": "10", + "pageIndex": "2", + }, + response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.Authorizations.SearchGroupMemberships(&AuthorizationsSearchGroupMembershipsOptions{ + PaginationParamsV2: PaginationParamsV2{PageIndex: 2, PageSize: 10}, + GroupId: "g1", + UserId: "u1", + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Len(t, result.GroupMemberships, 1) +} + +func TestAuthorizationsV2_SearchGroupMemberships_Validation(t *testing.T) { + client := newLocalhostClient(t) + + _, _, err := client.V2.Authorizations.SearchGroupMemberships(&AuthorizationsSearchGroupMembershipsOptions{ + PaginationParamsV2: PaginationParamsV2{PageSize: 600}, + }) + assert.Error(t, err) +} + +// ============================================================================= +// CreateGroupMembership +// ============================================================================= + +func TestAuthorizationsV2_CreateGroupMembership(t *testing.T) { + response := GroupMembership{ + Id: "m-new", + GroupId: "g1", + UserId: "u1", + } + server := newTestServer(t, mockJSONBodyHandler(t, http.MethodPost, "/v2/authorizations/group-memberships", http.StatusCreated, + &AuthorizationsCreateGroupMembershipOptions{ + GroupId: "g1", + UserId: "u1", + }, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.Authorizations.CreateGroupMembership(&AuthorizationsCreateGroupMembershipOptions{ + GroupId: "g1", + UserId: "u1", + }) + require.NoError(t, err) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + assert.Equal(t, "g1", result.GroupId) + assert.Equal(t, "u1", result.UserId) +} + +func TestAuthorizationsV2_CreateGroupMembership_Validation(t *testing.T) { + client := newLocalhostClient(t) + + tests := []struct { + name string + opt *AuthorizationsCreateGroupMembershipOptions + }{ + {"nil request", nil}, + {"missing group ID", &AuthorizationsCreateGroupMembershipOptions{UserId: "u1"}}, + {"missing user ID", &AuthorizationsCreateGroupMembershipOptions{GroupId: "g1"}}, + {"empty both", &AuthorizationsCreateGroupMembershipOptions{}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err := client.V2.Authorizations.CreateGroupMembership(tt.opt) + assert.Error(t, err) + }) + } +} + +// ============================================================================= +// DeleteGroupMembership +// ============================================================================= + +func TestAuthorizationsV2_DeleteGroupMembership(t *testing.T) { + server := newTestServer(t, mockEmptyHandler(t, http.MethodDelete, "/v2/authorizations/group-memberships/m1", http.StatusNoContent)) + client := newTestClient(t, server.url()) + + resp, err := client.V2.Authorizations.DeleteGroupMembership("m1") + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) +} + +func TestAuthorizationsV2_DeleteGroupMembership_Validation(t *testing.T) { + client := newLocalhostClient(t) + + _, err := client.V2.Authorizations.DeleteGroupMembership("") + assert.Error(t, err) +}