From d04001e2cba1d77c2d5f064522b2d2bf6a91d62a Mon Sep 17 00:00:00 2001 From: Marcel Blijleven Date: Wed, 23 Aug 2023 21:48:28 +0200 Subject: [PATCH] feat: add jira client --- pkg/jira/authentication.go | 15 ++++++ pkg/jira/client.go | 85 +++++++++++++++++++++++++++++++ pkg/jira/client_test.go | 85 +++++++++++++++++++++++++++++++ pkg/jira/data/issue_response.json | 49 ++++++++++++++++++ pkg/jira/response.go | 24 +++++++++ 5 files changed, 258 insertions(+) create mode 100644 pkg/jira/authentication.go create mode 100644 pkg/jira/client.go create mode 100644 pkg/jira/client_test.go create mode 100644 pkg/jira/data/issue_response.json create mode 100644 pkg/jira/response.go diff --git a/pkg/jira/authentication.go b/pkg/jira/authentication.go new file mode 100644 index 0000000..53b6539 --- /dev/null +++ b/pkg/jira/authentication.go @@ -0,0 +1,15 @@ +package jira + +import "net/http" + +// authenticationService is used to authenticate requests +type authenticationService struct { + client *Client + email string + token string +} + +// setBasisAuth sets the username and password on the provided request +func (a *authenticationService) setBasicAuth(req *http.Request) { + req.SetBasicAuth(a.email, a.token) +} diff --git a/pkg/jira/client.go b/pkg/jira/client.go new file mode 100644 index 0000000..9d22a18 --- /dev/null +++ b/pkg/jira/client.go @@ -0,0 +1,85 @@ +package jira + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" +) + +// Client allows the program to interact with the JIRA API +type Client struct { + host *url.URL + httpClient HttpClient + authentication *authenticationService +} + +// HttpClient is the http client interface used by the JIRA client +type HttpClient interface { + Do(req *http.Request) (*http.Response, error) +} + +func NewClient(host, email, token string, httpClient HttpClient) (*Client, error) { + if host == "" { + return nil, errors.New("could not create jira client: hostname cannot be empty") + } + + if email == "" { + return nil, errors.New("could not create jira client: email cannot be empty") + } + + if token == "" { + return nil, errors.New("could not create jira client: token cannot be empty") + } + + u, err := url.Parse(host) + + if err != nil { + return nil, fmt.Errorf("could not create jira client, invalid host provided: %w", err) + } + + client := &Client{host: u, httpClient: httpClient} + client.authentication = &authenticationService{ + client: client, + email: email, + token: token, + } + return client, nil +} + +func (c *Client) GetIssueDescription(key string) (*IssueDescriptionResponse, error) { + endpoint, _ := url.Parse(fmt.Sprintf("/rest/api/3/issue/%v?fields=description", key)) + u := c.host.ResolveReference(endpoint).String() + + req, err := http.NewRequest("GET", u, nil) + + if err != nil { + return nil, err + } + + c.authentication.setBasicAuth(req) + req.Header.Add("Content-Type", "application/json") + + res, err := c.httpClient.Do(req) + + if err != nil { + return nil, err + } + + var response IssueDescriptionResponse + + data, err := io.ReadAll(res.Body) + defer res.Body.Close() + + if err != nil { + return nil, err + } + + if err = json.Unmarshal(data, &response); err != nil { + return nil, err + } + + return &response, nil +} diff --git a/pkg/jira/client_test.go b/pkg/jira/client_test.go new file mode 100644 index 0000000..7e30654 --- /dev/null +++ b/pkg/jira/client_test.go @@ -0,0 +1,85 @@ +package jira + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "io" + "net/http" + "net/url" + "testing" +) + +type MockHttpClient struct { + t *testing.T + CalledMethod string + CalledWith []string + CalledTimes int + CalledHeaders http.Header + statusCode int +} + +func NewMockHttpClient(t *testing.T, statusCode int) *MockHttpClient { + return &MockHttpClient{t: t, statusCode: statusCode, CalledTimes: 0, CalledWith: []string{}} +} + +func (m *MockHttpClient) Do(req *http.Request) (*http.Response, error) { + m.CalledTimes += 1 + data, err := io.ReadAll(req.Body) + defer req.Body.Close() + + if err != nil { + m.t.Fatal("error occurred while doing mock request") + } + + m.CalledWith = append(m.CalledWith, string(data)) + m.CalledMethod = req.Method + m.CalledHeaders = req.Header + + if m.statusCode == http.StatusBadRequest { + body := bytes.NewReader([]byte("{\"errorMessages\":[],\"errors\":{\"name\":\"A version with this name already exists in this project.\"}}")) + return &http.Response{Status: "Bad request", StatusCode: http.StatusBadRequest, Body: io.NopCloser(body)}, nil + } + body := bytes.NewReader([]byte("{\"hello\": \"world\"}")) + return &http.Response{Status: "Created", StatusCode: http.StatusCreated, Body: io.NopCloser(body)}, nil +} + +func TestNewClient(t *testing.T) { + m := NewMockHttpClient(t, 200) + parsedHost, _ := url.Parse("https://test.nu") + + c, err := NewClient("https://test.nu", "marcel@test.nl", "c0ffee", m) + + assert.NoError(t, err) + + expected := &Client{ + host: parsedHost, + httpClient: m, + authentication: nil, + } + + expected.authentication = &authenticationService{ + client: expected, + email: "marcel@test.nl", + token: "c0ffee", + } + + assert.Equal(t, expected, c) +} + +func TestNewClient_missingHost(t *testing.T) { + c, err := NewClient("", "marcel@test.nl", "c0ffee", nil) + assert.Nil(t, c) + assert.EqualError(t, err, "could not create jira client: hostname cannot be empty") +} + +func TestNewClient_missingEmail(t *testing.T) { + c, err := NewClient("https://test.nu", "", "c0ffee", nil) + assert.Nil(t, c) + assert.EqualError(t, err, "could not create jira client: email cannot be empty") +} + +func TestNewClient_missingToken(t *testing.T) { + c, err := NewClient("https://test.nu", "marcel@test.nl", "", nil) + assert.Nil(t, c) + assert.EqualError(t, err, "could not create jira client: token cannot be empty") +} diff --git a/pkg/jira/data/issue_response.json b/pkg/jira/data/issue_response.json new file mode 100644 index 0000000..c24153a --- /dev/null +++ b/pkg/jira/data/issue_response.json @@ -0,0 +1,49 @@ +{ + "expand": "renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations", + "id": "56360", + "self": "https://foo.atlassian.net/rest/api/3/issue/56360", + "key": "MB-1337", + "fields": { + "description": { + "version": 1, + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "text": "The " + }, + { + "type": "text", + "text": "read_file", + "marks": [ + { + "type": "code" + } + ] + }, + { + "type": "text", + "text": " function in " + }, + { + "type": "text", + "text": "file_utils.py", + "marks": [ + { + "type": "code" + } + ] + }, + { + "type": "text", + "text": " uses demo text to provide test data" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/pkg/jira/response.go b/pkg/jira/response.go new file mode 100644 index 0000000..5e1fdf1 --- /dev/null +++ b/pkg/jira/response.go @@ -0,0 +1,24 @@ +package jira + +type IssueDescriptionResponse struct { + Expand string `json:"expand"` + Id string `json:"id"` + Self string `json:"self"` + Key string `json:"key"` + Fields struct { + Description struct { + Version int `json:"version"` + Type string `json:"type"` + Content []struct { + Type string `json:"type"` + Content []struct { + Type string `json:"type"` + Text string `json:"text"` + Marks []struct { + Type string `json:"type"` + } `json:"marks,omitempty"` + } `json:"content"` + } `json:"content"` + } `json:"description"` + } `json:"fields"` +}