Repository: ctreminiom/go-atlassian (go)

Issue:
Add group user picker
**Is your feature request related to a problem? Please describe.**
This does not exists.
https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-group-and-user-picker/#api-rest-api-2-groupuserpicker-get

**Additional context**
Neither in V2 nor V3


Pull request description:
Added GroupUserPicker service & Find funtion
Fixes #278 

Patch:
diff --git a/go.mod b/go.mod
index b751078..9ab3fb0 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.14
 
 require (
 	dario.cat/mergo v1.0.0
+	github.com/google/go-querystring v1.1.0
 	github.com/google/uuid v1.6.0
 	github.com/stretchr/testify v1.9.0
 	github.com/tidwall/gjson v1.17.1
diff --git a/go.sum b/go.sum
index 7855961..632583f 100644
--- a/go.sum
+++ b/go.sum
@@ -3,6 +3,10 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -23,6 +27,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/jira/internal/group_user_picker_impl.go b/jira/internal/group_user_picker_impl.go
new file mode 100644
index 0000000..ef879ce
--- /dev/null
+++ b/jira/internal/group_user_picker_impl.go
@@ -0,0 +1,71 @@
+package internal
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/google/go-querystring/query"
+
+	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/jira"
+)
+
+func NewGroupUserPickerService(client service.Connector, version string) (*GroupUserPickerService, error) {
+
+	if version == "" {
+		return nil, model.ErrNoVersionProvided
+	}
+
+	return &GroupUserPickerService{
+		internalClient: &internalGroupUserPickerServiceImpl{c: client, version: version},
+	}, nil
+}
+
+type GroupUserPickerService struct {
+	internalClient jira.GroupUserPickerConnector
+}
+
+// Find returns a list of users and groups matching a string.
+//
+// GET /rest/api/{2-3}/groupuserpicker
+//
+// https://docs.go-atlassian.io/jira-software-cloud/groupuserpicker#find-users-and-groups
+func (g *GroupUserPickerService) Find(ctx context.Context, options *model.GroupUserPickerFindOptionScheme) (*model.GroupUserPickerFindScheme, *model.ResponseScheme, error) {
+	return g.internalClient.Find(ctx, options)
+}
+
+type internalGroupUserPickerServiceImpl struct {
+	c       service.Connector
+	version string
+}
+
+func (i internalGroupUserPickerServiceImpl) Find(ctx context.Context, options *model.GroupUserPickerFindOptionScheme) (*model.GroupUserPickerFindScheme, *model.ResponseScheme, error) {
+
+	if options == nil || options.Query == "" {
+		return nil, nil, model.ErrNoQueryError
+	}
+
+	endpoint := fmt.Sprintf("rest/api/%v/groupuserpicker", i.version)
+
+	q, err := query.Values(options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	endpoint += "?" + q.Encode()
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	find := new(model.GroupUserPickerFindScheme)
+	response, err := i.c.Call(request, find)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return find, response, nil
+}
diff --git a/jira/v2/api_client_impl.go b/jira/v2/api_client_impl.go
index 91bbfe2..80d4b9e 100644
--- a/jira/v2/api_client_impl.go
+++ b/jira/v2/api_client_impl.go
@@ -5,13 +5,14 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"github.com/ctreminiom/go-atlassian/jira/internal"
-	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
-	"github.com/ctreminiom/go-atlassian/service/common"
 	"io"
 	"net/http"
 	"net/url"
 	"strings"
+
+	"github.com/ctreminiom/go-atlassian/jira/internal"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service/common"
 )
 
 const ApiVersion = "2"
@@ -67,6 +68,11 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 		return nil, err
 	}
 
+	groupUserPickerService, err := internal.NewGroupUserPickerService(client, ApiVersion)
+	if err != nil {
+		return nil, err
+	}
+
 	groupService, err := internal.NewGroupService(client, ApiVersion)
 	if err != nil {
 		return nil, err
@@ -374,6 +380,7 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 	client.Role = applicationRoleService
 	client.Dashboard = dashboardService
 	client.Filter = filterService
+	client.GroupUserPicker = groupUserPickerService
 	client.Group = groupService
 	client.Issue = issueService
 	client.Project = project
@@ -399,6 +406,7 @@ type Client struct {
 	Dashboard          *internal.DashboardService
 	Filter             *internal.FilterService
 	Group              *internal.GroupService
+	GroupUserPicker    *internal.GroupUserPickerService
 	Issue              *internal.IssueRichTextService
 	MySelf             *internal.MySelfService
 	Permission         *internal.PermissionService
diff --git a/jira/v3/api_client_impl.go b/jira/v3/api_client_impl.go
index b47cb70..f376e33 100644
--- a/jira/v3/api_client_impl.go
+++ b/jira/v3/api_client_impl.go
@@ -5,13 +5,14 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"github.com/ctreminiom/go-atlassian/jira/internal"
-	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
-	"github.com/ctreminiom/go-atlassian/service/common"
 	"io"
 	"net/http"
 	"net/url"
 	"strings"
+
+	"github.com/ctreminiom/go-atlassian/jira/internal"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service/common"
 )
 
 const ApiVersion = "3"
@@ -67,6 +68,11 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 		return nil, err
 	}
 
+	groupUserPickerService, err := internal.NewGroupUserPickerService(client, ApiVersion)
+	if err != nil {
+		return nil, err
+	}
+
 	groupService, err := internal.NewGroupService(client, ApiVersion)
 	if err != nil {
 		return nil, err
@@ -374,6 +380,7 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 	client.Role = applicationRoleService
 	client.Dashboard = dashboardService
 	client.Filter = filterService
+	client.GroupUserPicker = groupUserPickerService
 	client.Group = groupService
 	client.Issue = issueService
 	client.Project = project
@@ -399,6 +406,7 @@ type Client struct {
 	Dashboard          *internal.DashboardService
 	Filter             *internal.FilterService
 	Group              *internal.GroupService
+	GroupUserPicker    *internal.GroupUserPickerService
 	Issue              *internal.IssueADFService
 	MySelf             *internal.MySelfService
 	Permission         *internal.PermissionService
diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go
index 7e27230..71ca72f 100644
--- a/pkg/infra/models/errors.go
+++ b/pkg/infra/models/errors.go
@@ -129,6 +129,7 @@ var (
 	ErrNoFieldConfigurationIDError         = errors.New("jira: no field configuration id set")
 	ErrNoFieldConfigurationSchemeNameError = errors.New("jira: no field configuration scheme name set")
 	ErrNoFieldConfigurationSchemeIDError   = errors.New("jira: no field configuration scheme id set")
+	ErrNoQueryError                        = errors.New("jira: no query set")
 	ErrNoVersionProvided                   = errors.New("client: no module version set")
 	ErrNoIssueTypeSchemeIDError            = errors.New("jira: no issue type scheme id set")
 	ErrNoTaskIDError                       = errors.New("atlassian: no task id set")
diff --git a/pkg/infra/models/jira_group_user_picker.go b/pkg/infra/models/jira_group_user_picker.go
new file mode 100644
index 0000000..b93e3f0
--- /dev/null
+++ b/pkg/infra/models/jira_group_user_picker.go
@@ -0,0 +1,86 @@
+package models
+
+type GroupUserPickerFindOptionScheme struct {
+	// The search string.
+	Query string `url:"query"`
+	// The maximum number of items to return in each list.
+	MaxResults int `url:"maxResults,omitempty"`
+	// Whether the user avatar should be returned.
+	ShowAvatar bool `url:"showAvatar,omitempty"`
+	// The custom field ID of the field this request is for.
+	FieldID string `url:"fieldId,omitempty"`
+	// The ID of a project that returned users and groups must have permission to view.
+	// This parameter is only used when FieldID is present.
+	ProjectIDs []string `url:"projectId,omitempty"`
+	// The ID of an issue type that returned users and groups must have permission to view.
+	// Special values, such as -1 (all standard issue types) and -2 (all subtask issue types), are supported.
+	// This parameter is only used when FieldID is present.
+	IssueTypeIDs []string `url:"issueTypeId,omitempty"`
+	// The size of the avatar to return.
+	// Possible values are:
+	// xsmall, xsmall@2x, xsmall@3x, small, small@2x, small@3x, medium, medium@2x, medium@3x, large,
+	// large@2x, large@3x, xlarge, xlarge@2x, xlarge@3x, xxlarge, xxlarge@2x, xxlarge@3x, xxxlarge,
+	// xxxlarge@2x, xxxlarge@3x
+	AvatarSize string `url:"avatarSize,omitempty"`
+	// Whether the search for groups should be case-insensitive.
+	CaseInsensitive bool `url:"caseInsensitive,omitempty"`
+	// Whether Connect app users and groups should be excluded from the search results.
+	ExcludeConnectAddons bool `url:"excludeConnectAddons,omitempty"`
+}
+
+type GroupUserPickerFindScheme struct {
+	// The list of groups found in a search, including header text (Showing X of Y matching groups)
+	// and total of matched groups.
+	Groups *GroupUserPickerFoundGroupsScheme `json:"groups"`
+	Users  *GroupUserPickerFoundUsersScheme  `json:"users"`
+}
+
+type GroupUserPickerFoundGroupsScheme struct {
+	Groups []*GroupUserPickerFoundGroupScheme `json:"groups"`
+	// Header text indicating the number of groups in the response and the total number of groups found in the search.
+	Header string `json:"header"`
+	// The total number of groups found in the search.
+	Total int `json:"total"`
+}
+
+type GroupUserPickerFoundGroupScheme struct {
+	// The ID of the group, which uniquely identifies the group across all Atlassian products.
+	// For example, 952d12c3-5b5b-4d04-bb32-44d383afc4b2.
+	GroupID string `json:"groupId"`
+	// The group name with the matched query string highlighted with the HTML bold tag.
+	HTML   string                                  `json:"html"`
+	Labels []*GroupUserPickerFoundGroupLabelScheme `json:"labels"`
+	// The name of the group.
+	// The name of a group is mutable, to reliably identify a group use GroupID.
+	Name string `json:"name"`
+}
+
+type GroupUserPickerFoundGroupLabelScheme struct {
+	// The group label name.
+	Text string `json:"text"`
+	// The title of the group label.
+	Title string `json:"title"`
+	// The type of the group label.
+	// Valid values: ADMIN, SINGLE, MULTIPLE
+	Type string `json:"type"`
+}
+
+type GroupUserPickerFoundUsersScheme struct {
+	Users []*GroupUserPickerFoundUserScheme `json:"users"`
+	// Header text indicating the number of groups in the response and the total number of groups found in the search.
+	Header string `json:"header"`
+	// The total number of groups found in the search.
+	Total int `json:"total"`
+}
+
+type GroupUserPickerFoundUserScheme struct {
+	// The account ID of the user, which uniquely identifies the user across all Atlassian products.
+	// For example, 5b10ac8d82e05b22cc7d4ef5.
+	AccountID string `json:"accountId"`
+	// The avatar URL of the user.
+	AvatarURL string `json:"avatarUrl"`
+	//  The display name of the user. Depending on the user’s privacy setting, this may be returned as null.
+	DisplayName string `json:"displayName"`
+	// The display name, email address, and key of the user with the matched query string highlighted with the HTML bold tag.
+	HTML string `json:"html"`
+}
diff --git a/service/jira/group_user_picker.go b/service/jira/group_user_picker.go
new file mode 100644
index 0000000..1f8f576
--- /dev/null
+++ b/service/jira/group_user_picker.go
@@ -0,0 +1,18 @@
+package jira
+
+import (
+	"context"
+
+	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
+)
+
+// GroupUserPickerConnector is an interface that defines the methods available from GroupUserPickerConnector API.
+type GroupUserPickerConnector interface {
+
+	// Find returns a list of users and groups matching a string.
+	//
+	// GET /rest/api/{2-3}/groupuserpicker
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/groupuserpicker#find-users-and-groups
+	Find(ctx context.Context, options *model.GroupUserPickerFindOptionScheme) (*model.GroupUserPickerFindScheme, *model.ResponseScheme, error)
+}


Test patch:
diff --git a/jira/internal/group_user_picker_impl_test.go b/jira/internal/group_user_picker_impl_test.go
new file mode 100644
index 0000000..f70b57b
--- /dev/null
+++ b/jira/internal/group_user_picker_impl_test.go
@@ -0,0 +1,160 @@
+package internal
+
+import (
+	"context"
+	"errors"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/mocks"
+)
+
+func Test_internalGroupUserPickerServiceImpl_Find(t *testing.T) {
+
+	type fields struct {
+		c       service.Connector
+		version string
+	}
+
+	type args struct {
+		ctx     context.Context
+		options *model.GroupUserPickerFindOptionScheme
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the api version is v2",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:     context.Background(),
+				options: &model.GroupUserPickerFindOptionScheme{Query: "jira"},
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/groupuserpicker?query=jira",
+					"",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.GroupUserPickerFindScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name:   "when the api version is v3",
+			fields: fields{version: "3"},
+			args: args{
+				ctx:     context.Background(),
+				options: &model.GroupUserPickerFindOptionScheme{Query: "jira"},
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/3/groupuserpicker?query=jira",
+					"",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.GroupUserPickerFindScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name:   "when the query is not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:     context.Background(),
+				options: &model.GroupUserPickerFindOptionScheme{},
+			},
+			on: func(fields *fields) {
+				fields.c = mocks.NewConnector(t)
+			},
+			wantErr: true,
+			Err:     model.ErrNoQueryError,
+		},
+
+		{
+			name:   "when the http request cannot be created",
+			fields: fields{version: "3"},
+			args: args{
+				ctx:     context.Background(),
+				options: &model.GroupUserPickerFindOptionScheme{Query: "jira"},
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/3/groupuserpicker?query=jira",
+					"",
+					nil).
+					Return(&http.Request{}, errors.New("error, unable to create the http request"))
+
+				fields.c = client
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to create the http request"),
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			groupUserPickerService, err := NewGroupUserPickerService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResult, gotResponse, err := groupUserPickerService.Find(testCase.args.ctx, testCase.args.options)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+			}
+
+		})
+	}
+}