From 301799676509a97140035e01801de23c2b5e1f6b Mon Sep 17 00:00:00 2001 From: ahmadsalimi Date: Mon, 8 Feb 2021 03:53:28 +0330 Subject: [PATCH] Feature: Add support for Jira ServiceDesk and its organizations --- jira.go | 4 + organization.go | 387 +++++++++++++++++++++++++++++++++++++++++++ organization_test.go | 337 +++++++++++++++++++++++++++++++++++++ servicedesk.go | 114 +++++++++++++ servicedesk_test.go | 102 ++++++++++++ 5 files changed, 944 insertions(+) create mode 100644 organization.go create mode 100644 organization_test.go create mode 100644 servicedesk.go create mode 100644 servicedesk_test.go diff --git a/jira.go b/jira.go index abca201..784f34f 100644 --- a/jira.go +++ b/jira.go @@ -56,6 +56,8 @@ type Client struct { PermissionScheme *PermissionSchemeService Status *StatusService IssueLinkType *IssueLinkTypeService + Organization *OrganizationService + ServiceDesk *ServiceDeskService } // NewClient returns a new Jira API client. @@ -102,6 +104,8 @@ func NewClient(httpClient httpClient, baseURL string) (*Client, error) { c.PermissionScheme = &PermissionSchemeService{client: c} c.Status = &StatusService{client: c} c.IssueLinkType = &IssueLinkTypeService{client: c} + c.Organization = &OrganizationService{client: c} + c.ServiceDesk = &ServiceDeskService{client: c} return c, nil } diff --git a/organization.go b/organization.go new file mode 100644 index 0000000..090594c --- /dev/null +++ b/organization.go @@ -0,0 +1,387 @@ +package jira + +import ( + "context" + "fmt" +) + +// OrganizationService handles Organizations for the Jira instance / API. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/ +type OrganizationService struct { + client *Client +} + +// OrganizationCreationDTO is DTO for creat organization API +type OrganizationCreationDTO struct { + Name string `json:"name,omitempty" structs:"name,omitempty"` +} + +// SelfLink Stores REST API URL to the organization. +type SelfLink struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` +} + +// Organization contains Organization data +type Organization struct { + ID string `json:"id,omitempty" structs:"id,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Links *SelfLink `json:"_links,omitempty" structs:"_links,omitempty"` +} + +// OrganizationUsersDTO contains organization user ids +type OrganizationUsersDTO struct { + AccountIds []string `json:"accountIds,omitempty" structs:"accountIds,omitempty"` +} + +// PagedDTO is response of a paged list +type PagedDTO struct { + Size int `json:"size,omitempty" structs:"size,omitempty"` + Start int `json:"start,omitempty" structs:"start,omitempty"` + Limit int `limit:"size,omitempty" structs:"limit,omitempty"` + IsLastPage bool `json:"isLastPage,omitempty" structs:"isLastPage,omitempty"` + Values []interface{} `values:"isLastPage,omitempty" structs:"values,omitempty"` + Expands []string `json:"_expands,omitempty" structs:"_expands,omitempty"` +} + +// PropertyKey contains Property key details. +type PropertyKey struct { + Self string `json:"self,omitempty" structs:"self,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` +} + +// PropertyKeys contains an array of PropertyKey +type PropertyKeys struct { + Keys []PropertyKey `json:"keys,omitempty" structs:"keys,omitempty"` +} + +// GetAllOrganizationsWithContext returns a list of organizations in +// the Jira Service Management instance. +// Use this method when you want to present a list +// of organizations or want to locate an organization +// by name. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-group-organization +func (s *OrganizationService) GetAllOrganizationsWithContext(ctx context.Context, start int, limit int, accountID string) (*PagedDTO, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization?start=%d&limit=%d", start, limit) + if accountID != "" { + apiEndPoint += fmt.Sprintf("&accountId=%s", accountID) + } + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + v := new(PagedDTO) + resp, err := s.client.Do(req, v) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return v, resp, nil +} + +// GetAllOrganizations wraps GetAllOrganizationsWithContext using the background context. +func (s *OrganizationService) GetAllOrganizations(start int, limit int, accountID string) (*PagedDTO, *Response, error) { + return s.GetAllOrganizationsWithContext(context.Background(), start, limit, accountID) +} + +// CreateOrganizationWithContext creates an organization by +// passing the name of the organization. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-post +func (s *OrganizationService) CreateOrganizationWithContext(ctx context.Context, name string) (*Organization, *Response, error) { + apiEndPoint := "rest/servicedeskapi/organization" + + organization := OrganizationCreationDTO{ + Name: name, + } + + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, organization) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + o := new(Organization) + resp, err := s.client.Do(req, &o) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return o, resp, nil +} + +// CreateOrganization wraps CreateOrganizationWithContext using the background context. +func (s *OrganizationService) CreateOrganization(name string) (*Organization, *Response, error) { + return s.CreateOrganizationWithContext(context.Background(), name) +} + +// GetOrganizationWithContext returns details of an +// organization. Use this method to get organization +// details whenever your application component is +// passed an organization ID but needs to display +// other organization details. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-get +func (s *OrganizationService) GetOrganizationWithContext(ctx context.Context, organizationID int) (*Organization, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + o := new(Organization) + resp, err := s.client.Do(req, &o) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return o, resp, nil +} + +// GetOrganization wraps GetOrganizationWithContext using the background context. +func (s *OrganizationService) GetOrganization(organizationID int) (*Organization, *Response, error) { + return s.GetOrganizationWithContext(context.Background(), organizationID) +} + +// DeleteOrganizationWithContext deletes an organization. Note that +// the organization is deleted regardless +// of other associations it may have. +// For example, associations with service desks. +// +// Jira API docs: https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-delete +func (s *OrganizationService) DeleteOrganizationWithContext(ctx context.Context, organizationID int) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// DeleteOrganization wraps DeleteOrganizationWithContext using the background context. +func (s *OrganizationService) DeleteOrganization(organizationID int) (*Response, error) { + return s.DeleteOrganizationWithContext(context.Background(), organizationID) +} + +// GetPropertiesKeysWithContext returns the keys of +// all properties for an organization. Use this resource +// when you need to find out what additional properties +// items have been added to an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-get +func (s *OrganizationService) GetPropertiesKeysWithContext(ctx context.Context, organizationID int) (*PropertyKeys, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + pk := new(PropertyKeys) + resp, err := s.client.Do(req, &pk) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return pk, resp, nil +} + +// GetPropertiesKeys wraps GetPropertiesKeysWithContext using the background context. +func (s *OrganizationService) GetPropertiesKeys(organizationID int) (*PropertyKeys, *Response, error) { + return s.GetPropertiesKeysWithContext(context.Background(), organizationID) +} + +// GetPropertyWithContext returns the value of a property +// from an organization. Use this method to obtain the JSON +// content for an organization's property. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-get +func (s *OrganizationService) GetPropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*EntityProperty, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + ep := new(EntityProperty) + resp, err := s.client.Do(req, &ep) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return ep, resp, nil +} + +// GetProperty wraps GetPropertyWithContext using the background context. +func (s *OrganizationService) GetProperty(organizationID int, propertyKey string) (*EntityProperty, *Response, error) { + return s.GetPropertyWithContext(context.Background(), organizationID, propertyKey) +} + +// SetPropertyWithContext sets the value of a +// property for an organization. Use this +// resource to store custom data against an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-put +func (s *OrganizationService) SetPropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey) + + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// SetProperty wraps SetPropertyWithContext using the background context. +func (s *OrganizationService) SetProperty(organizationID int, propertyKey string) (*Response, error) { + return s.SetPropertyWithContext(context.Background(), organizationID, propertyKey) +} + +// DeletePropertyWithContext removes a property from an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-property-propertykey-delete +func (s *OrganizationService) DeletePropertyWithContext(ctx context.Context, organizationID int, propertyKey string) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/property/%s", organizationID, propertyKey) + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// DeleteProperty wraps DeletePropertyWithContext using the background context. +func (s *OrganizationService) DeleteProperty(organizationID int, propertyKey string) (*Response, error) { + return s.DeletePropertyWithContext(context.Background(), organizationID, propertyKey) +} + +// GetUsersWithContext returns all the users +// associated with an organization. Use this +// method where you want to provide a list of +// users for an organization or determine if +// a user is associated with an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-get +func (s *OrganizationService) GetUsersWithContext(ctx context.Context, organizationID int, start int, limit int) (*PagedDTO, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user?start=%d&limit=%d", organizationID, start, limit) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + users := new(PagedDTO) + resp, err := s.client.Do(req, &users) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return users, resp, nil +} + +// GetUsers wraps GetUsersWithContext using the background context. +func (s *OrganizationService) GetUsers(organizationID int, start int, limit int) (*PagedDTO, *Response, error) { + return s.GetUsersWithContext(context.Background(), organizationID, start, limit) +} + +// AddUsersWithContext adds users to an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-post +func (s *OrganizationService) AddUsersWithContext(ctx context.Context, organizationID int, users OrganizationUsersDTO) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, users) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// AddUsers wraps AddUsersWithContext using the background context. +func (s *OrganizationService) AddUsers(organizationID int, users OrganizationUsersDTO) (*Response, error) { + return s.AddUsersWithContext(context.Background(), organizationID, users) +} + +// RemoveUsersWithContext removes users from an organization. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-organization-organizationid-user-delete +func (s *OrganizationService) RemoveUsersWithContext(ctx context.Context, organizationID int, users OrganizationUsersDTO) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/organization/%d/user", organizationID) + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// RemoveUsers wraps RemoveUsersWithContext using the background context. +func (s *OrganizationService) RemoveUsers(organizationID int, users OrganizationUsersDTO) (*Response, error) { + return s.RemoveUsersWithContext(context.Background(), organizationID, users) +} diff --git a/organization_test.go b/organization_test.go new file mode 100644 index 0000000..49910b9 --- /dev/null +++ b/organization_test.go @@ -0,0 +1,337 @@ +package jira + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" +) + +func TestOrganizationService_GetAllOrganizationsWithContext(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/servicedeskapi/organization") + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ "_expands": [], "size": 1, "start": 1, "limit": 1, "isLastPage": false, "_links": { "base": "https://your-domain.atlassian.net/rest/servicedeskapi", "context": "context", "next": "https://your-domain.atlassian.net/rest/servicedeskapi/organization?start=2&limit=1", "prev": "https://your-domain.atlassian.net/rest/servicedeskapi/organization?start=0&limit=1" }, "values": [ { "id": "1", "name": "Charlie Cakes Franchises", "_links": { "self": "https://your-domain.atlassian.net/rest/servicedeskapi/organization/1" } } ] }`) + }) + + result, _, err := testClient.Organization.GetAllOrganizations(0, 50, "") + + if result == nil { + t.Error("Expected Organizations. Result is nil") + } + + if result.Size != 1 { + t.Errorf("Expected size to be 1, but got %d", result.Size) + } + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestOrganizationService_CreateOrganization(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testRequestURL(t, r, "/rest/servicedeskapi/organization") + + o := new(OrganizationCreationDTO) + json.NewDecoder(r.Body).Decode(&o) + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ "id": "1", "name": "%s", "_links": { "self": "https://your-domain.atlassian.net/rest/servicedeskapi/organization/1" } }`, o.Name) + }) + + name := "MyOrg" + o, _, err := testClient.Organization.CreateOrganization(name) + + if o == nil { + t.Error("Expected Organization. Result is nil") + } + + if o.Name != name { + t.Errorf("Expected name to be %s, but got %s", name, o.Name) + } + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestOrganizationService_GetOrganization(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ "id": "1", "name": "name", "_links": { "self": "https://your-domain.atlassian.net/rest/servicedeskapi/organization/1" } }`) + }) + + id := 1 + o, _, err := testClient.Organization.GetOrganization(id) + + if err != nil { + t.Errorf("Error given: %s", err) + } + + if o == nil { + t.Error("Expected Organization. Result is nil") + } + + if o.Name != "name" { + t.Errorf("Expected name to be name, but got %s", o.Name) + } +} + +func TestOrganizationService_DeleteOrganization(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1") + + w.WriteHeader(http.StatusNoContent) + }) + + _, err := testClient.Organization.DeleteOrganization(1) + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestOrganizationService_GetPropertiesKeys(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1/property", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1/property") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "keys": [ + { + "self": "/rest/servicedeskapi/organization/1/property/propertyKey", + "key": "organization.attributes" + } + ] + }`) + }) + + pk, _, err := testClient.Organization.GetPropertiesKeys(1) + + if err != nil { + t.Errorf("Error given: %s", err) + } + + if pk == nil { + t.Error("Expected Keys. Result is nil") + } + + if pk.Keys[0].Key != "organization.attributes" { + t.Errorf("Expected name to be organization.attributes, but got %s", pk.Keys[0].Key) + } +} + +func TestOrganizationService_GetProperty(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1/property/organization.attributes", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1/property/organization.attributes") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "key": "organization.attributes", + "value": { + "phone": "0800-1233456789", + "mail": "charlie@example.com" + } + }`) + }) + + key := "organization.attributes" + ep, _, err := testClient.Organization.GetProperty(1, key) + + if err != nil { + t.Errorf("Error given: %s", err) + } + + if ep == nil { + t.Error("Expected Entity. Result is nil") + } + + if ep.Key != key { + t.Errorf("Expected name to be %s, but got %s", key, ep.Key) + } +} + +func TestOrganizationService_SetProperty(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1/property/organization.attributes", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1/property/organization.attributes") + + w.WriteHeader(http.StatusOK) + }) + + key := "organization.attributes" + _, err := testClient.Organization.SetProperty(1, key) + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestOrganizationService_DeleteProperty(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1/property/organization.attributes", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1/property/organization.attributes") + + w.WriteHeader(http.StatusOK) + }) + + key := "organization.attributes" + _, err := testClient.Organization.DeleteProperty(1, key) + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestOrganizationService_GetUsers(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1/user", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1/user") + + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, `{ + "_expands": [], + "size": 1, + "start": 1, + "limit": 1, + "isLastPage": false, + "_links": { + "base": "https://your-domain.atlassian.net/rest/servicedeskapi", + "context": "context", + "next": "https://your-domain.atlassian.net/rest/servicedeskapi/organization/1/user?start=2&limit=1", + "prev": "https://your-domain.atlassian.net/rest/servicedeskapi/organization/1/user?start=0&limit=1" + }, + "values": [ + { + "accountId": "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3581db05e2a66fa80b", + "name": "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3581db05e2a66fa80b", + "key": "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3581db05e2a66fa80b", + "emailAddress": "fred@example.com", + "displayName": "Fred F. User", + "active": true, + "timeZone": "Australia/Sydney", + "_links": { + "jiraRest": "https://your-domain.atlassian.net/rest/api/2/user?username=qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3581db05e2a66fa80b", + "avatarUrls": { + "48x48": "https://avatar-cdn.atlassian.com/9bc3b5bcb0db050c6d7660b28a5b86c9?s=48", + "24x24": "https://avatar-cdn.atlassian.com/9bc3b5bcb0db050c6d7660b28a5b86c9?s=24", + "16x16": "https://avatar-cdn.atlassian.com/9bc3b5bcb0db050c6d7660b28a5b86c9?s=16", + "32x32": "https://avatar-cdn.atlassian.com/9bc3b5bcb0db050c6d7660b28a5b86c9?s=32" + }, + "self": "https://your-domain.atlassian.net/rest/api/2/user?username=qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3581db05e2a66fa80b" + } + }, + { + "accountId": "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3a01db05e2a66fa80bd", + "name": "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3a01db05e2a66fa80bd", + "key": "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3a01db05e2a66fa80bd", + "emailAddress": "bob@example.com", + "displayName": "Bob D. Builder", + "active": true, + "timeZone": "Australia/Sydney", + "_links": { + "jiraRest": "https://your-domain.atlassian.net/rest/api/2/user?username=qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3a01db05e2a66fa80bd", + "avatarUrls": { + "48x48": "https://avatar-cdn.atlassian.com/9bc3b5bcb0db050c6d7660b28a5b86c9?s=48", + "24x24": "https://avatar-cdn.atlassian.com/9bc3b5bcb0db050c6d7660b28a5b86c9?s=24", + "16x16": "https://avatar-cdn.atlassian.com/9bc3b5bcb0db050c6d7660b28a5b86c9?s=16", + "32x32": "https://avatar-cdn.atlassian.com/9bc3b5bcb0db050c6d7660b28a5b86c9?s=32" + }, + "self": "https://your-domain.atlassian.net/rest/api/2/user?username=qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3a01db05e2a66fa80bd" + } + } + ] + }`) + }) + + users, _, err := testClient.Organization.GetUsers(1, 0, 50) + + if err != nil { + t.Errorf("Error given: %s", err) + } + + if users == nil { + t.Error("Expected Organizations. Result is nil") + } + + if users.Size != 1 { + t.Errorf("Expected size to be 1, but got %d", users.Size) + } + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestOrganizationService_AddUsers(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1/user", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1/user") + + w.WriteHeader(http.StatusNoContent) + }) + + users := OrganizationUsersDTO{ + AccountIds: []string{ + "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3581db05e2a66fa80b", + "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3a01db05e2a66fa80bd", + }, + } + _, err := testClient.Organization.AddUsers(1, users) + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestOrganizationService_RemoveUsers(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/organization/1/user", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testRequestURL(t, r, "/rest/servicedeskapi/organization/1/user") + + w.WriteHeader(http.StatusNoContent) + }) + + users := OrganizationUsersDTO{ + AccountIds: []string{ + "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3581db05e2a66fa80b", + "qm:a713c8ea-1075-4e30-9d96-891a7d181739:5ad6d3a01db05e2a66fa80bd", + }, + } + _, err := testClient.Organization.RemoveUsers(1, users) + + if err != nil { + t.Errorf("Error given: %s", err) + } +} diff --git a/servicedesk.go b/servicedesk.go new file mode 100644 index 0000000..c6c9147 --- /dev/null +++ b/servicedesk.go @@ -0,0 +1,114 @@ +package jira + +import ( + "context" + "fmt" +) + +// ServiceDeskService handles ServiceDesk for the Jira instance / API. +type ServiceDeskService struct { + client *Client +} + +// ServiceDeskOrganizationDTO is a DTO for ServiceDesk organizations +type ServiceDeskOrganizationDTO struct { + OrganizationID int `json:"organizationId,omitempty" structs:"organizationId,omitempty"` +} + +// GetOrganizationsWithContext returns a list of +// all organizations associated with a service desk. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-get +func (s *ServiceDeskService) GetOrganizationsWithContext(ctx context.Context, serviceDeskID int, start int, limit int, accountID string) (*PagedDTO, *Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization?start=%d&limit=%d", serviceDeskID, start, limit) + if accountID != "" { + apiEndPoint += fmt.Sprintf("&accountId=%s", accountID) + } + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) + req.Header.Set("Accept", "application/json") + + if err != nil { + return nil, nil, err + } + + orgs := new(PagedDTO) + resp, err := s.client.Do(req, &orgs) + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return orgs, resp, nil +} + +// GetOrganizations wraps GetOrganizationsWithContext using the background context. +func (s *ServiceDeskService) GetOrganizations(serviceDeskID int, start int, limit int, accountID string) (*PagedDTO, *Response, error) { + return s.GetOrganizationsWithContext(context.Background(), serviceDeskID, start, limit, accountID) +} + +// AddOrganizationWithContext adds an organization to +// a service desk. If the organization ID is already +// associated with the service desk, no change is made +// and the resource returns a 204 success code. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-post +func (s *ServiceDeskService) AddOrganizationWithContext(ctx context.Context, serviceDeskID int, organizationID int) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization", serviceDeskID) + + organization := ServiceDeskOrganizationDTO{ + OrganizationID: organizationID, + } + + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, organization) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// AddOrganization wraps AddOrganizationWithContext using the background context. +func (s *ServiceDeskService) AddOrganization(serviceDeskID int, organizationID int) (*Response, error) { + return s.AddOrganizationWithContext(context.Background(), serviceDeskID, organizationID) +} + +// RemoveOrganizationWithContext removes an organization +// from a service desk. If the organization ID does not +// match an organization associated with the service desk, +// no change is made and the resource returns a 204 success code. +// +// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-delete +func (s *ServiceDeskService) RemoveOrganizationWithContext(ctx context.Context, serviceDeskID int, organizationID int) (*Response, error) { + apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization", serviceDeskID) + + organization := ServiceDeskOrganizationDTO{ + OrganizationID: organizationID, + } + + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, organization) + + if err != nil { + return nil, err + } + + resp, err := s.client.Do(req, nil) + if err != nil { + jerr := NewJiraError(resp, err) + return resp, jerr + } + + return resp, nil +} + +// RemoveOrganization wraps RemoveOrganizationWithContext using the background context. +func (s *ServiceDeskService) RemoveOrganization(serviceDeskID int, organizationID int) (*Response, error) { + return s.RemoveOrganizationWithContext(context.Background(), serviceDeskID, organizationID) +} diff --git a/servicedesk_test.go b/servicedesk_test.go new file mode 100644 index 0000000..7e519be --- /dev/null +++ b/servicedesk_test.go @@ -0,0 +1,102 @@ +package jira + +import ( + "fmt" + "net/http" + "testing" +) + +func TestServiceDeskService_GetOrganizations(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/servicedesk/10001/organization", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/servicedeskapi/servicedesk/10001/organization") + + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "_expands": [], + "size": 3, + "start": 3, + "limit": 3, + "isLastPage": false, + "_links": { + "base": "https://your-domain.atlassian.net/rest/servicedeskapi", + "context": "context", + "next": "https://your-domain.atlassian.net/rest/servicedeskapi/servicedesk/10001/organization?start=6&limit=3", + "prev": "https://your-domain.atlassian.net/rest/servicedeskapi/servicedesk/10001/organization?start=0&limit=3" + }, + "values": [ + { + "id": "1", + "name": "Charlie Cakes Franchises", + "_links": { + "self": "https://your-domain.atlassian.net/rest/servicedeskapi/organization/1" + } + }, + { + "id": "2", + "name": "Atlas Coffee Co", + "_links": { + "self": "https://your-domain.atlassian.net/rest/servicedeskapi/organization/2" + } + }, + { + "id": "3", + "name": "The Adjustment Bureau", + "_links": { + "self": "https://your-domain.atlassian.net/rest/servicedeskapi/organization/3" + } + } + ] + }`) + }) + + orgs, _, err := testClient.ServiceDesk.GetOrganizations(10001, 3, 3, "") + + if orgs == nil { + t.Error("Expected Organizations. Result is nil") + } + + if orgs.Size != 3 { + t.Errorf("Expected size to be 3, but got %d", orgs.Size) + } + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestServiceDeskService_AddOrganizations(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/servicedesk/10001/organization", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testRequestURL(t, r, "/rest/servicedeskapi/servicedesk/10001/organization") + + w.WriteHeader(http.StatusNoContent) + }) + + _, err := testClient.ServiceDesk.AddOrganization(10001, 1) + + if err != nil { + t.Errorf("Error given: %s", err) + } +} + +func TestServiceDeskService_RemoveOrganizations(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/servicedeskapi/servicedesk/10001/organization", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testRequestURL(t, r, "/rest/servicedeskapi/servicedesk/10001/organization") + + w.WriteHeader(http.StatusNoContent) + }) + + _, err := testClient.ServiceDesk.RemoveOrganization(10001, 1) + + if err != nil { + t.Errorf("Error given: %s", err) + } +}