Repository: PagerDuty/go-pagerduty
Pull request #543 (2024-10-22 14:23:25)

Add support for Jira Cloud accounts mappings and rules
This PR adds support to the endpoints in the Jira Cloud Integration API:

- `GET /integration-jira-cloud/account_mappings` - [List Account Mappings](https://developer.pagerduty.com/api-reference/5c25164c9df6c-list-account-mappings).
- `GET /integration-jira-cloud/account_mappings/{id}` - [Get an Account Mapping](https://developer.pagerduty.com/api-reference/8d707b61562b7-get-an-account-mapping).
- `GET /integration-jira-cloud/account_mappings/{id}/rules` - [List Rules](https://developer.pagerduty.com/api-reference/954c93fe1c64f-list-rules).
- `POST /integration-jira-cloud/account_mappings/{id}/rules` - [Create a New Rule](https://developer.pagerduty.com/api-reference/85dc30ba966a6-create-a-rule).
- `GET /integration-jira-cloud/account_mappings/{id}/rules` - [Get a Rule](https://developer.pagerduty.com/api-reference/4f98e318e2bb3-get-a-rule).
- `UPDATE /integration-jira-cloud/account_mappings/{id}/rules` - [Update a New Rule](https://developer.pagerduty.com/api-reference/bce6c10fa07d7-update-a-rule).
- `DELETE /integration-jira-cloud/account_mappings/{id}/rules` - [Delete a Rule](https://developer.pagerduty.com/api-reference/0d18a54b67671-delete-a-rule).

<img width="306" alt="image" src="https://github.com/user-attachments/assets/d4d27b18-967a-407b-9d99-1c160315fdaf">



Patch:
diff --git a/jira_cloud_accounts_mapping.go b/jira_cloud_accounts_mapping.go
new file mode 100644
index 0000000..7f4b07b
--- /dev/null
+++ b/jira_cloud_accounts_mapping.go
@@ -0,0 +1,262 @@
+package pagerduty
+
+import (
+	"context"
+
+	"github.com/google/go-querystring/query"
+)
+
+// JiraCloudAccountsMapping establishes a connection between a PagerDuty account
+// and a Jira Cloud instance, enabling integration and synchronization between
+// the two platforms
+type JiraCloudAccountsMapping struct {
+	UpdatedAt        string           `json:"updated_at,omitempty"`
+	CreatedAt        string           `json:"created_at,omitempty"`
+	ID               string           `json:"id,omitempty"`
+	JiraCloudAccount JiraCloudAccount `json:"jira_cloud_account"`
+	PagerDutyAccount PagerDutyAccount `json:"pagerduty_account"`
+}
+
+// JiraCloudAccount describes an account from Jira
+type JiraCloudAccount struct {
+	// The base URL of the Jira Cloud instance, used for API calls and
+	// constructing links
+	BaseURL string `json:"base_url"`
+}
+
+// PagerDutyAccount describes an account from PagerDuty
+type PagerDutyAccount struct {
+	// The unique subdomain of the PagerDuty account, used to identify and
+	// access the account (e.g., acme in https://acme.pagerduty.com)
+	Subdomain string `json:"subdomain"`
+}
+
+// JiraCloudAccountsMappingRule configures the bidirectional synchronization between Jira
+// issues and PagerDuty incidents
+type JiraCloudAccountsMappingRule struct {
+	AccountsMapping             *APIObject                         `json:"account_mapping,omitempty"`
+	AutocreateJqlDisabledReason string                             `json:"autocreate_jql_disabled_reason,omitempty"`
+	AutocreateJqlDisabledUntil  string                             `json:"autocreate_jql_disabled_until,omitempty"`
+	Config                      JiraCloudAccountsMappingRuleConfig `json:"config"`
+	ID                          string                             `json:"id,omitempty"`
+	Name                        string                             `json:"name"`
+	CreatedAt                   string                             `json:"created_at,omitempty"`
+	UpdatedAt                   string                             `json:"updated_at,omitempty"`
+}
+
+// JiraCloudAccountsMappingRuleConfig is the configuration for bidirectional synchronization
+// between Jira issues and PagerDuty incidents
+type JiraCloudAccountsMappingRuleConfig struct {
+	Jira    JiraCloudSettings `json:"jira"`
+	Service APIObject         `json:"service"`
+}
+
+// JiraCloudSettings are settings for the Jira aspect of the
+// synchronization
+type JiraCloudSettings struct {
+	AutocreateJQL                *string                `json:"autocreate_jql"`
+	CreateIssueOnIncidentTrigger bool                   `json:"create_issue_on_incident_trigger"`
+	CustomFields                 []JiraCloudCustomField `json:"custom_fields"`
+	IssueType                    JiraCloudReference     `json:"issue_type"`
+	Priorities                   []JiraCloudPriority    `json:"priorities"`
+	Project                      JiraCloudReference     `json:"project"`
+	StatusMapping                JiraCloudStatusMapping `json:"status_mapping"`
+	SyncNotesUser                *UserJiraCloud         `json:"sync_notes_user"`
+}
+
+// JiraCloudCustomField defines how Jira fields are populated when a Jira Issue
+// is created from a PagerDuty Incident
+type JiraCloudCustomField struct {
+	// The PagerDuty incident field from which the value will be extracted
+	// (only applicable if type is attribute)
+	//
+	// Allowed values:
+	// incident_number, incident_title, incident_description,
+	// incident_status, incident_created_at, incident_service,
+	// incident_escalation_policy, incident_impacted_services,
+	// incident_html_url, incident_assignees, incident_acknowledgers,
+	// incident_last_status_change_at, incident_last_status_change_by,
+	// incident_urgency, incident_priority, null
+	SourceIncidentField *string `json:"source_incident_field"`
+
+	// The unique identifier key of the Jira field that will be set
+	TargetIssueField string `json:"target_issue_field"`
+
+	// The human-readable name of the Jira field
+	TargetIssueFieldName string `json:"target_issue_field_name"`
+
+	// The type of the value that will be set
+	//
+	// Allowed values:
+	// attribute, const, jira_value
+	Type string `json:"type"`
+
+	// The value to be set for the Jira field (only applicable if type is
+	// const or jira_value)
+	Value interface{} `json:"value,omitempty"`
+}
+
+// JiraCloudReference is a reference pointing to a Jira Cloud object
+type JiraCloudReference struct {
+	ID   string `json:"id"`
+	Name string `json:"name"`
+	Key  string `json:"key,omitempty"`
+}
+
+// JiraCloudPriority is an association between a PagerDuty incident priority
+// and a Jira issue priority
+type JiraCloudPriority struct {
+	JiraID      string `json:"jira_id"`
+	PagerDutyID string `json:"pagerduty_id"`
+}
+
+// JiraCloudStatusMapping is an association between PagerDuty incident statuses
+// to their corresponding Jira issue statuses
+type JiraCloudStatusMapping struct {
+	Acknowledged *JiraCloudReference `json:"acknowledged"`
+	Resolved     *JiraCloudReference `json:"resolved"`
+	Triggered    *JiraCloudReference `json:"triggered"`
+}
+
+// UserJiraCloud is a PagerDuty user for syncing notes and comments between Jira
+// issues and PagerDuty incidents. If not provided, note synchronization is
+// disabled
+type UserJiraCloud struct {
+	APIObject
+	Email string `json:"email,omitempty"`
+}
+
+// ListJiraCloudAccountsMappingsOptions are the options available when calling the ListJiraCloudAccountsMappings API endpoint
+type ListJiraCloudAccountsMappingsOptions struct {
+	Limit  uint `url:"limit,omitempty"`
+	Offset uint `url:"offset,omitempty"`
+	Total  bool `url:"total,omitempty"`
+}
+
+// ListJiraCloudAccountsMappingsResponse is the response when calling the ListJiraCloudAccountsMappings API endpoint
+type ListJiraCloudAccountsMappingsResponse struct {
+	APIListObject
+	AccountsMappings []JiraCloudAccountsMapping `json:"accounts_mappings"`
+}
+
+// ListJiraCloudAccountsMappings lists existing account mappings
+func (c *Client) ListJiraCloudAccountsMappings(ctx context.Context, o ListJiraCloudAccountsMappingsOptions) (*ListJiraCloudAccountsMappingsResponse, error) {
+	v, err := query.Values(o)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := c.get(ctx, "/integration-jira-cloud/accounts_mappings?"+v.Encode(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var result ListJiraCloudAccountsMappingsResponse
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// GetJiraCloudAccountsMapping lists existing account mappings
+func (c *Client) GetJiraCloudAccountsMapping(ctx context.Context, id string) (*JiraCloudAccountsMapping, error) {
+	resp, err := c.get(ctx, "/integration-jira-cloud/accounts_mappings/"+id, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var result JiraCloudAccountsMapping
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// ListJiraCloudAccountsMappingRulesOptions are the options available when
+// calling the ListJiraCloudAccountsMappingRules API endpoint
+type ListJiraCloudAccountsMappingRulesOptions struct {
+	Limit  uint `url:"limit,omitempty"`
+	Offset uint `url:"offset,omitempty"`
+	Total  bool `url:"total,omitempty"`
+}
+
+// ListJiraCloudAccountsMappingRulesResponse is the response when calling the
+// ListJiraCloudAccountsMappingRules API endpoint
+type ListJiraCloudAccountsMappingRulesResponse struct {
+	APIListObject
+	Rules []JiraCloudAccountsMappingRule `json:"rules"`
+}
+
+// ListJiraCloudAccountsMappingRules lists existing rules for a specific account
+// mapping
+func (c *Client) ListJiraCloudAccountsMappingRules(ctx context.Context, id string, o ListJiraCloudAccountsMappingRulesOptions) (*ListJiraCloudAccountsMappingsResponse, error) {
+	v, err := query.Values(o)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := c.get(ctx, "/integration-jira-cloud/accounts_mappings/"+id+"/rules?"+v.Encode(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var result ListJiraCloudAccountsMappingsResponse
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// CreateJiraCloudAccountsMappingRule creates a new rule in Jira Cloud's integration
+func (c *Client) CreateJiraCloudAccountsMappingRule(ctx context.Context, id string, rule JiraCloudAccountsMappingRule) (*JiraCloudAccountsMappingRule, error) {
+	resp, err := c.post(ctx, "/integration-jira-cloud/accounts_mappings/"+id+"/rules", rule, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var result JiraCloudAccountsMappingRule
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// GetJiraCloudAccountsMappingRule gets detailed information about an existing rule
+func (c *Client) GetJiraCloudAccountsMappingRule(ctx context.Context, id, ruleID string) (*JiraCloudAccountsMappingRule, error) {
+	resp, err := c.get(ctx, "/integration-jira-cloud/accounts_mappings/"+id+"/rules/"+ruleID, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var result JiraCloudAccountsMappingRule
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}
+
+// DeleteJiraCloudAccountsMappingRule deletes an existing rule in Jira Cloud's integration
+func (c *Client) DeleteJiraCloudAccountsMappingRule(ctx context.Context, id, ruleID string) error {
+	_, err := c.delete(ctx, "/integration-jira-cloud/accounts_mappings/"+id+"/rules/"+ruleID)
+	return err
+}
+
+// UpdateJiraCloudAccountsMappingRule updates an existing rule in Jira Cloud's integration
+func (c *Client) UpdateJiraCloudAccountsMappingRule(ctx context.Context, id string, rule JiraCloudAccountsMappingRule) (*JiraCloudAccountsMappingRule, error) {
+	resp, err := c.put(ctx, "/integration-jira-cloud/accounts_mappings/"+id+"/rules/"+rule.ID, rule, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var result JiraCloudAccountsMappingRule
+	if err = c.decodeJSON(resp, &result); err != nil {
+		return nil, err
+	}
+
+	return &result, nil
+}


Test patch:
diff --git a/jira_cloud_accounts_mapping_test.go b/jira_cloud_accounts_mapping_test.go
new file mode 100644
index 0000000..59b2f72
--- /dev/null
+++ b/jira_cloud_accounts_mapping_test.go
@@ -0,0 +1,210 @@
+package pagerduty
+
+import (
+	"context"
+	"net/http"
+	"testing"
+)
+
+func TestJiraCloudAccountsMapping_List(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/integration-jira-cloud/accounts_mappings/", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		_, _ = w.Write([]byte(`{
+  "accounts_mappings": [
+    {
+      "created_at": "2024-09-12T12:34:55.000Z",
+      "id": "PIJ90N7",
+      "jira_cloud_account": {
+        "base_url": "https://atlassian-subdomain.atlassian.net"
+      },
+      "pagerduty_account": {
+        "subdomain": "pagerduty-subdomain"
+      },
+      "updated_at": "2024-09-13T10:11:12.000Z"
+    }
+  ],
+  "limit": 25,
+  "more": false,
+  "offset": 0,
+  "total": 1
+}`))
+	})
+	client := defaultTestClient(server.URL, "foo")
+
+	want := &ListJiraCloudAccountsMappingsResponse{
+		APIListObject: APIListObject{
+			Limit:  25,
+			More:   false,
+			Offset: 0,
+			Total:  1,
+		},
+		AccountsMappings: []JiraCloudAccountsMapping{
+			{
+				ID: "PIJ90N7",
+				JiraCloudAccount: JiraCloudAccount{
+					BaseURL: "https://atlassian-subdomain.atlassian.net",
+				},
+				PagerDutyAccount: PagerDutyAccount{
+					Subdomain: "pagerduty-subdomain",
+				},
+				CreatedAt: "2024-09-12T12:34:55.000Z",
+				UpdatedAt: "2024-09-13T10:11:12.000Z",
+			},
+		},
+	}
+
+	ctx := context.Background()
+	res, err := client.ListJiraCloudAccountsMappings(ctx, ListJiraCloudAccountsMappingsOptions{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}
+
+func TestJiraCloudAccountsMapping_Get(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/integration-jira-cloud/accounts_mappings/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		_, _ = w.Write([]byte(`{
+  "created_at": "2024-09-12T12:34:55.000Z",
+  "id": "PIJ90N7",
+  "jira_cloud_account": {
+    "base_url": "https://atlassian-subdomain.atlassian.net"
+  },
+  "pagerduty_account": {
+    "subdomain": "pagerduty-subdomain"
+  },
+  "updated_at": "2024-09-13T10:11:12.000Z"
+}`))
+	})
+	client := defaultTestClient(server.URL, "foo")
+
+	want := &JiraCloudAccountsMapping{
+		ID: "PIJ90N7",
+		JiraCloudAccount: JiraCloudAccount{
+			BaseURL: "https://atlassian-subdomain.atlassian.net",
+		},
+		PagerDutyAccount: PagerDutyAccount{
+			Subdomain: "pagerduty-subdomain",
+		},
+		CreatedAt: "2024-09-12T12:34:55.000Z",
+		UpdatedAt: "2024-09-13T10:11:12.000Z",
+	}
+
+	ctx := context.Background()
+	res, err := client.GetJiraCloudAccountsMapping(ctx, "1")
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}
+
+func TestJiraCloudAccountsMappingRule_Create(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/integration-jira-cloud/accounts_mappings/1/rules", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		_, _ = w.Write([]byte(`{"id": "2"}`))
+	})
+	client := defaultTestClient(server.URL, "foo")
+
+	want := JiraCloudAccountsMappingRule{ID: "2"}
+
+	ctx := context.Background()
+	res, err := client.CreateJiraCloudAccountsMappingRule(ctx, "1", want)
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, &want, res)
+}
+
+func TestJiraCloudAccountsMappingRule_List(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/integration-jira-cloud/accounts_mappings/1/rules", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		_, _ = w.Write([]byte(`{"rules": [], "limit": 25, "more": false, "offset": 0, "total": 0}`))
+	})
+	client := defaultTestClient(server.URL, "foo")
+
+	want := &ListJiraCloudAccountsMappingsResponse{
+		APIListObject: APIListObject{
+			Limit:  25,
+			More:   false,
+			Offset: 0,
+			Total:  0,
+		},
+		AccountsMappings: nil,
+	}
+
+	ctx := context.Background()
+	res, err := client.ListJiraCloudAccountsMappingRules(ctx, "1", ListJiraCloudAccountsMappingRulesOptions{})
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}
+
+func TestJiraCloudAccountsMappingRule_Get(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/integration-jira-cloud/accounts_mappings/1/rules/2", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		_, _ = w.Write([]byte(`{"id": "PF9KMXH"}`))
+	})
+	client := defaultTestClient(server.URL, "foo")
+
+	want := &JiraCloudAccountsMappingRule{ID: "PF9KMXH"}
+
+	ctx := context.Background()
+	res, err := client.GetJiraCloudAccountsMappingRule(ctx, "1", "2")
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, want, res)
+}
+
+func TestJiraCloudAccountsMappingRule_Delete(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/integration-jira-cloud/accounts_mappings/1/rules/2", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		w.WriteHeader(http.StatusNoContent)
+	})
+	client := defaultTestClient(server.URL, "foo")
+
+	ctx := context.Background()
+	err := client.DeleteJiraCloudAccountsMappingRule(ctx, "1", "2")
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestJiraCloudAccountsMappingRule_Update(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/integration-jira-cloud/accounts_mappings/1/rules/2", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PUT")
+		_, _ = w.Write([]byte(`{"id": "2"}`))
+	})
+	client := defaultTestClient(server.URL, "foo")
+
+	want := JiraCloudAccountsMappingRule{ID: "2"}
+
+	ctx := context.Background()
+	res, err := client.UpdateJiraCloudAccountsMappingRule(ctx, "1", want)
+	if err != nil {
+		t.Fatal(err)
+	}
+	testEqual(t, &want, res)
+}