diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml new file mode 100644 index 00000000..88eb1e3e --- /dev/null +++ b/.github/workflows/greetings.yml @@ -0,0 +1,12 @@ +name: Greetings +on: [pull_request, issues] + +jobs: + greeting: + runs-on: ubuntu-latest + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Hi! Thank you for taking the time to create your first issue! Really cool to see you here for the first time. Please give us a bit of time to review it.' + pr-message: 'Great! Thank you for taking the time to create your first pull request. It is always a pleasure to see people like you who spent time contributing. Please give us a bit of time to review it!' \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..c8ab7a18 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,19 @@ +name: "Close stale issues" +on: + schedule: + - cron: "0 4 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v1.1.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity in the last 60 days. It will be closed in 7 days if no further activity occurs. + Thank you for your contributions. + days-before-stale: 60 + days-before-close: 7 + stale-issue-label: stale \ No newline at end of file diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 00000000..a1316476 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,43 @@ +name: Tests + +on: + push: + branches: + - master + pull_request: + +jobs: + test: + name: Test and lint + strategy: + matrix: + go-version: [1.x, 1.13.x, 1.12.x] + platform: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: 1.14 + + # Caching go modules to speed up the run + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Run go fmt + if: runner.os != 'Windows' + run: diff -u <(echo -n) <(gofmt -d -s .) + + - name: Run go vet + run: make vet + + - name: Run staticcheck + run: make staticcheck + + - name: Run Unit tests. + run: make test \ No newline at end of file diff --git a/authentication.go b/authentication.go index 3c182204..bd123b90 100644 --- a/authentication.go +++ b/authentication.go @@ -1,6 +1,7 @@ package jira import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -47,7 +48,7 @@ type Session struct { Cookies []*http.Cookie } -// AcquireSessionCookie creates a new session for a user in Jira. +// AcquireSessionCookieWithContext creates a new session for a user in Jira. // Once a session has been successfully created it can be used to access any of Jira's remote APIs and also the web UI by passing the appropriate HTTP Cookie header. // The header will by automatically applied to every API request. // Note that it is generally preferrable to use HTTP BASIC authentication with the REST API. @@ -56,7 +57,7 @@ type Session struct { // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session // // Deprecated: Use CookieAuthTransport instead -func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) { +func (s *AuthenticationService) AcquireSessionCookieWithContext(ctx context.Context, username, password string) (bool, error) { apiEndpoint := "rest/auth/1/session" body := struct { Username string `json:"username"` @@ -66,7 +67,7 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string) password, } - req, err := s.client.NewRequest("POST", apiEndpoint, body) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, body) if err != nil { return false, err } @@ -79,10 +80,10 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string) } if err != nil { - return false, fmt.Errorf("Auth at Jira instance failed (HTTP(S) request). %s", err) + return false, fmt.Errorf("auth at Jira instance failed (HTTP(S) request). %s", err) } if resp != nil && resp.StatusCode != 200 { - return false, fmt.Errorf("Auth at Jira instance failed (HTTP(S) request). Status code: %d", resp.StatusCode) + return false, fmt.Errorf("auth at Jira instance failed (HTTP(S) request). Status code: %d", resp.StatusCode) } s.client.session = session @@ -91,6 +92,13 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string) return true, nil } +// AcquireSessionCookie wraps AcquireSessionCookieWithContext using the background context. +// +// Deprecated: Use CookieAuthTransport instead +func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) { + return s.AcquireSessionCookieWithContext(context.Background(), username, password) +} + // SetBasicAuth sets username and password for the basic auth against the Jira instance. // // Deprecated: Use BasicAuthTransport instead @@ -113,29 +121,29 @@ func (s *AuthenticationService) Authenticated() bool { return false } -// Logout logs out the current user that has been authenticated and the session in the client is destroyed. +// LogoutWithContext logs out the current user that has been authenticated and the session in the client is destroyed. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session // // Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the // client anymore -func (s *AuthenticationService) Logout() error { +func (s *AuthenticationService) LogoutWithContext(ctx context.Context) error { if s.authType != authTypeSession || s.client.session == nil { return fmt.Errorf("no user is authenticated") } apiEndpoint := "rest/auth/1/session" - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { - return fmt.Errorf("Creating the request to log the user out failed : %s", err) + return fmt.Errorf("creating the request to log the user out failed : %s", err) } resp, err := s.client.Do(req, nil) if err != nil { - return fmt.Errorf("Error sending the logout request: %s", err) + return fmt.Errorf("error sending the logout request: %s", err) } if resp.StatusCode != 204 { - return fmt.Errorf("The logout was unsuccessful with status %d", resp.StatusCode) + return fmt.Errorf("the logout was unsuccessful with status %d", resp.StatusCode) } // If logout successful, delete session @@ -145,43 +153,56 @@ func (s *AuthenticationService) Logout() error { } -// GetCurrentUser gets the details of the current user. +// Logout wraps LogoutWithContext using the background context. +// +// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the +// client anymore +func (s *AuthenticationService) Logout() error { + return s.LogoutWithContext(context.Background()) +} + +// GetCurrentUserWithContext gets the details of the current user. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session -func (s *AuthenticationService) GetCurrentUser() (*Session, error) { +func (s *AuthenticationService) GetCurrentUserWithContext(ctx context.Context) (*Session, error) { if s == nil { - return nil, fmt.Errorf("AUthenticaiton Service is not instantiated") + return nil, fmt.Errorf("authentication Service is not instantiated") } if s.authType != authTypeSession || s.client.session == nil { - return nil, fmt.Errorf("No user is authenticated yet") + return nil, fmt.Errorf("no user is authenticated yet") } apiEndpoint := "rest/auth/1/session" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { - return nil, fmt.Errorf("Could not create request for getting user info : %s", err) + return nil, fmt.Errorf("could not create request for getting user info : %s", err) } resp, err := s.client.Do(req, nil) if err != nil { - return nil, fmt.Errorf("Error sending request to get user info : %s", err) + return nil, fmt.Errorf("error sending request to get user info : %s", err) } if resp.StatusCode != 200 { - return nil, fmt.Errorf("Getting user info failed with status : %d", resp.StatusCode) + return nil, fmt.Errorf("getting user info failed with status : %d", resp.StatusCode) } defer resp.Body.Close() ret := new(Session) data, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("Couldn't read body from the response : %s", err) + return nil, fmt.Errorf("couldn't read body from the response : %s", err) } err = json.Unmarshal(data, &ret) if err != nil { - return nil, fmt.Errorf("Could not unmarshall received user info : %s", err) + return nil, fmt.Errorf("could not unmarshall received user info : %s", err) } return ret, nil } + +// GetCurrentUser wraps GetCurrentUserWithContext using the background context. +func (s *AuthenticationService) GetCurrentUser() (*Session, error) { + return s.GetCurrentUserWithContext(context.Background()) +} diff --git a/authentication_test.go b/authentication_test.go index 3841532f..431a7fdc 100644 --- a/authentication_test.go +++ b/authentication_test.go @@ -19,10 +19,10 @@ func TestAuthenticationService_AcquireSessionCookie_Failure(t *testing.T) { if err != nil { t.Errorf("Error in read body: %s", err) } - if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + if !bytes.Contains(b, []byte(`"username":"foo"`)) { t.Error("No username found") } - if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + if !bytes.Contains(b, []byte(`"password":"bar"`)) { t.Error("No password found") } @@ -53,10 +53,10 @@ func TestAuthenticationService_AcquireSessionCookie_Success(t *testing.T) { if err != nil { t.Errorf("Error in read body: %s", err) } - if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + if !bytes.Contains(b, []byte(`"username":"foo"`)) { t.Error("No username found") } - if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + if !bytes.Contains(b, []byte(`"password":"bar"`)) { t.Error("No password found") } @@ -144,10 +144,10 @@ func TestAithenticationService_GetUserInfo_AccessForbidden_Fail(t *testing.T) { if err != nil { t.Errorf("Error in read body: %s", err) } - if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + if !bytes.Contains(b, []byte(`"username":"foo"`)) { t.Error("No username found") } - if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + if !bytes.Contains(b, []byte(`"password":"bar"`)) { t.Error("No password found") } @@ -182,10 +182,10 @@ func TestAuthenticationService_GetUserInfo_NonOkStatusCode_Fail(t *testing.T) { if err != nil { t.Errorf("Error in read body: %s", err) } - if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + if !bytes.Contains(b, []byte(`"username":"foo"`)) { t.Error("No username found") } - if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + if !bytes.Contains(b, []byte(`"password":"bar"`)) { t.Error("No password found") } @@ -238,10 +238,10 @@ func TestAuthenticationService_GetUserInfo_Success(t *testing.T) { if err != nil { t.Errorf("Error in read body: %s", err) } - if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + if !bytes.Contains(b, []byte(`"username":"foo"`)) { t.Error("No username found") } - if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + if !bytes.Contains(b, []byte(`"password":"bar"`)) { t.Error("No password found") } @@ -280,10 +280,10 @@ func TestAuthenticationService_Logout_Success(t *testing.T) { if err != nil { t.Errorf("Error in read body: %s", err) } - if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + if !bytes.Contains(b, []byte(`"username":"foo"`)) { t.Error("No username found") } - if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + if !bytes.Contains(b, []byte(`"password":"bar"`)) { t.Error("No password found") } diff --git a/board.go b/board.go index c1ac83a0..27516872 100644 --- a/board.go +++ b/board.go @@ -1,6 +1,7 @@ package jira import ( + "context" "fmt" "strconv" "time" @@ -124,16 +125,16 @@ type BoardConfigurationColumnStatus struct { Self string `json:"self"` } -// GetAllBoards will returns all boards. This only includes boards that the user has permission to view. +// GetAllBoardsWithContext will returns all boards. This only includes boards that the user has permission to view. // // Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards -func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) { +func (s *BoardService) GetAllBoardsWithContext(ctx context.Context, opt *BoardListOptions) (*BoardsList, *Response, error) { apiEndpoint := "rest/agile/1.0/board" url, err := addOptions(apiEndpoint, opt) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", url, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err } @@ -148,13 +149,18 @@ func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Respon return boards, resp, err } -// GetBoard will returns the board for the given boardID. +// GetAllBoards wraps GetAllBoardsWithContext using the background context. +func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) { + return s.GetAllBoardsWithContext(context.Background(), opt) +} + +// GetBoardWithContext will returns the board for the given boardID. // This board will only be returned if the user has permission to view it. // // Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard -func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { +func (s *BoardService) GetBoardWithContext(ctx context.Context, boardID int) (*Board, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -169,7 +175,12 @@ func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { return board, resp, nil } -// CreateBoard creates a new board. Board name, type and filter Id is required. +// GetBoard wraps GetBoardWithContext using the background context. +func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { + return s.GetBoardWithContext(context.Background(), boardID) +} + +// CreateBoardWithContext creates a new board. Board name, type and filter Id is required. // name - Must be less than 255 characters. // type - Valid values: scrum, kanban // filterId - Id of a filter that the user has permissions to view. @@ -177,9 +188,9 @@ func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) { // board will be created instead (remember that board sharing depends on the filter sharing). // // Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard -func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) { +func (s *BoardService) CreateBoardWithContext(ctx context.Context, board *Board) (*Board, *Response, error) { apiEndpoint := "rest/agile/1.0/board" - req, err := s.client.NewRequest("POST", apiEndpoint, board) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, board) if err != nil { return nil, nil, err } @@ -194,12 +205,17 @@ func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) { return responseBoard, resp, nil } -// DeleteBoard will delete an agile board. +// CreateBoard wraps CreateBoardWithContext using the background context. +func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) { + return s.CreateBoardWithContext(context.Background(), board) +} + +// DeleteBoardWithContext will delete an agile board. // // Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard -func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) { +func (s *BoardService) DeleteBoardWithContext(ctx context.Context, boardID int) (*Board, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -211,11 +227,16 @@ func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) { return nil, resp, err } -// GetAllSprints will return all sprints from a board, for a given board Id. +// DeleteBoard wraps DeleteBoardWithContext using the background context. +func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) { + return s.DeleteBoardWithContext(context.Background(), boardID) +} + +// GetAllSprintsWithContext will return all sprints from a board, for a given board Id. // This only includes sprints that the user has permission to view. // // Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint -func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) { +func (s *BoardService) GetAllSprintsWithContext(ctx context.Context, boardID string) ([]Sprint, *Response, error) { id, err := strconv.Atoi(boardID) if err != nil { return nil, nil, err @@ -229,17 +250,22 @@ func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error return result.Values, response, nil } -// GetAllSprintsWithOptions will return sprints from a board, for a given board Id and filtering options +// GetAllSprints wraps GetAllSprintsWithContext using the background context. +func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) { + return s.GetAllSprintsWithContext(context.Background(), boardID) +} + +// GetAllSprintsWithOptionsWithContext will return sprints from a board, for a given board Id and filtering options // This only includes sprints that the user has permission to view. // // Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint -func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) { +func (s *BoardService) GetAllSprintsWithOptionsWithContext(ctx context.Context, boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/sprint", boardID) url, err := addOptions(apiEndpoint, options) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("GET", url, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err } @@ -253,12 +279,17 @@ func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSpri return result, resp, err } -// GetBoardConfiguration will return a board configuration for a given board Id +// GetAllSprintsWithOptions wraps GetAllSprintsWithOptionsWithContext using the background context. +func (s *BoardService) GetAllSprintsWithOptions(boardID int, options *GetAllSprintsOptions) (*SprintsList, *Response, error) { + return s.GetAllSprintsWithOptionsWithContext(context.Background(), boardID, options) +} + +// GetBoardConfigurationWithContext will return a board configuration for a given board Id // Jira API docs:https://developer.atlassian.com/cloud/jira/software/rest/#api-rest-agile-1-0-board-boardId-configuration-get -func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, *Response, error) { +func (s *BoardService) GetBoardConfigurationWithContext(ctx context.Context, boardID int) (*BoardConfiguration, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%d/configuration", boardID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -273,3 +304,8 @@ func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, return result, resp, err } + +// GetBoardConfiguration wraps GetBoardConfigurationWithContext using the background context. +func (s *BoardService) GetBoardConfiguration(boardID int) (*BoardConfiguration, *Response, error) { + return s.GetBoardConfigurationWithContext(context.Background(), boardID) +} diff --git a/board_test.go b/board_test.go index 287e15fd..982f29bb 100644 --- a/board_test.go +++ b/board_test.go @@ -204,13 +204,13 @@ func TestBoardService_GetAllSprintsWithOptions(t *testing.T) { }) sprints, _, err := testClient.Board.GetAllSprintsWithOptions(123, &GetAllSprintsOptions{State: "active,future"}) - if err != nil { t.Errorf("Got error: %v", err) } if sprints == nil { t.Error("Expected sprint list. Got nil.") + return } if len(sprints.Values) != 1 { @@ -235,13 +235,13 @@ func TestBoardService_GetBoardConfigoration(t *testing.T) { }) boardConfiguration, _, err := testClient.Board.GetBoardConfiguration(35) - if err != nil { t.Errorf("Got error: %v", err) } if boardConfiguration == nil { t.Error("Expected boardConfiguration. Got nil.") + return } if len(boardConfiguration.ColumnConfig.Columns) != 6 { diff --git a/component.go b/component.go index 87e7fe3c..b76fe0cf 100644 --- a/component.go +++ b/component.go @@ -1,7 +1,8 @@ package jira -// ComponentService handles components for the Jira instance / API. -// +import "context" + +// ComponentService handles components for the Jira instance / API.// // Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.1/#api/2/component type ComponentService struct { client *Client @@ -19,10 +20,10 @@ type CreateComponentOptions struct { ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` } -// Create creates a new Jira component based on the given options. -func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) { +// CreateWithContext creates a new Jira component based on the given options. +func (s *ComponentService) CreateWithContext(ctx context.Context, options *CreateComponentOptions) (*ProjectComponent, *Response, error) { apiEndpoint := "rest/api/2/component" - req, err := s.client.NewRequest("POST", apiEndpoint, options) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, options) if err != nil { return nil, nil, err } @@ -36,3 +37,8 @@ func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComp return component, resp, nil } + +// Create wraps CreateWithContext using the background context. +func (s *ComponentService) Create(options *CreateComponentOptions) (*ProjectComponent, *Response, error) { + return s.CreateWithContext(context.Background(), options) +} diff --git a/error.go b/error.go index 833ee3be..c7bc2e58 100644 --- a/error.go +++ b/error.go @@ -34,12 +34,12 @@ func NewJiraError(resp *Response, httpError error) error { if strings.HasPrefix(contentType, "application/json") { err = json.Unmarshal(body, &jerr) if err != nil { - httpError = errors.Wrap(errors.New("Could not parse JSON"), httpError.Error()) + httpError = errors.Wrap(errors.New("could not parse JSON"), httpError.Error()) return errors.Wrap(err, httpError.Error()) } } else { if httpError == nil { - return fmt.Errorf("Got Response Status %s:%s", resp.Status, string(body)) + return fmt.Errorf("got response status %s:%s", resp.Status, string(body)) } return errors.Wrap(httpError, fmt.Sprintf("%s: %s", resp.Status, string(body))) } diff --git a/error_test.go b/error_test.go index cd3ad223..fab8b0fe 100644 --- a/error_test.go +++ b/error_test.go @@ -96,8 +96,8 @@ func TestError_BadJSON(t *testing.T) { err := NewJiraError(resp, errors.New("Original http error")) msg := err.Error() - if !strings.Contains(msg, "Could not parse JSON") { - t.Errorf("Expected the 'Could not parse JSON' error message: Got\n%s\n", msg) + if !strings.Contains(msg, "could not parse JSON") { + t.Errorf("Expected the 'could not parse JSON' error message: Got\n%s\n", msg) } } diff --git a/examples/jql/main.go b/examples/jql/main.go new file mode 100644 index 00000000..9cb05f54 --- /dev/null +++ b/examples/jql/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + + jira "github.com/andygrunwald/go-jira" +) + +func main() { + jiraClient, _ := jira.NewClient(nil, "https://issues.apache.org/jira/") + + // Running JQL query + + jql := "project = Mesos and type = Bug and Status NOT IN (Resolved)" + fmt.Printf("Usecase: Running a JQL query '%s'\n", jql) + issues, resp, err := jiraClient.Issue.Search(jql, nil) + if err != nil { + panic(err) + } + outputResponse(issues, resp) + + fmt.Println("") + fmt.Println("") + + // Running an empty JQL query to get all tickets + jql = "" + fmt.Printf("Usecase: Running an empty JQL query to get all tickets\n") + issues, resp, err = jiraClient.Issue.Search(jql, nil) + if err != nil { + panic(err) + } + outputResponse(issues, resp) +} + +func outputResponse(issues []jira.Issue, resp *jira.Response) { + fmt.Printf("Call to %s\n", resp.Request.URL) + fmt.Printf("Response Code: %d\n", resp.StatusCode) + fmt.Println("==================================") + for _, i := range issues { + fmt.Printf("%s (%s/%s): %+v\n", i.Key, i.Fields.Type.Name, i.Fields.Priority.Name, i.Fields.Summary) + } +} diff --git a/examples/renderedfields/main.go b/examples/renderedfields/main.go index ab23420d..d2e69cf8 100644 --- a/examples/renderedfields/main.go +++ b/examples/renderedfields/main.go @@ -48,7 +48,7 @@ func main() { return } - fmt.Printf("Targetting %s for issue %s\n", strings.TrimSpace(jiraURL), key) + fmt.Printf("Targeting %s for issue %s\n", strings.TrimSpace(jiraURL), key) options := &jira.GetQueryOptions{Expand: "renderedFields"} u, _, err := client.Issue.Get(key, options) diff --git a/field.go b/field.go index b8da9a75..28416734 100644 --- a/field.go +++ b/field.go @@ -1,5 +1,7 @@ package jira +import "context" + // FieldService handles fields for the Jira instance / API. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Field @@ -24,12 +26,12 @@ type FieldSchema struct { System string `json:"system,omitempty" structs:"system,omitempty"` } -// GetList gets all fields from Jira +// GetListWithContext gets all fields from Jira // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-field-get -func (s *FieldService) GetList() ([]Field, *Response, error) { +func (s *FieldService) GetListWithContext(ctx context.Context) ([]Field, *Response, error) { apiEndpoint := "rest/api/2/field" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -41,3 +43,8 @@ func (s *FieldService) GetList() ([]Field, *Response, error) { } return fieldList, resp, nil } + +// GetList wraps GetListWithContext using the background context. +func (s *FieldService) GetList() ([]Field, *Response, error) { + return s.GetListWithContext(context.Background()) +} diff --git a/filter.go b/filter.go index 720d2f8b..12ebe883 100644 --- a/filter.go +++ b/filter.go @@ -1,6 +1,7 @@ package jira import ( + "context" "fmt" "github.com/google/go-querystring/query" @@ -119,12 +120,12 @@ type FilterSearchOptions struct { Expand string `url:"expand,omitempty"` } -// GetList retrieves all filters from Jira -func (fs *FilterService) GetList() ([]*Filter, *Response, error) { +// GetListWithContext retrieves all filters from Jira +func (fs *FilterService) GetListWithContext(ctx context.Context) ([]*Filter, *Response, error) { options := &GetQueryOptions{} apiEndpoint := "rest/api/2/filter" - req, err := fs.client.NewRequest("GET", apiEndpoint, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -146,10 +147,15 @@ func (fs *FilterService) GetList() ([]*Filter, *Response, error) { return filters, resp, err } -// GetFavouriteList retrieves the user's favourited filters from Jira -func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) { +// GetList wraps GetListWithContext using the background context. +func (fs *FilterService) GetList() ([]*Filter, *Response, error) { + return fs.GetListWithContext(context.Background()) +} + +// GetFavouriteListWithContext retrieves the user's favourited filters from Jira +func (fs *FilterService) GetFavouriteListWithContext(ctx context.Context) ([]*Filter, *Response, error) { apiEndpoint := "rest/api/2/filter/favourite" - req, err := fs.client.NewRequest("GET", apiEndpoint, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -162,10 +168,15 @@ func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) { return filters, resp, err } -// Get retrieves a single Filter from Jira -func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) { +// GetFavouriteList wraps GetFavouriteListWithContext using the background context. +func (fs *FilterService) GetFavouriteList() ([]*Filter, *Response, error) { + return fs.GetFavouriteListWithContext(context.Background()) +} + +// GetWithContext retrieves a single Filter from Jira +func (fs *FilterService) GetWithContext(ctx context.Context, filterID int) (*Filter, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/filter/%d", filterID) - req, err := fs.client.NewRequest("GET", apiEndpoint, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -179,16 +190,21 @@ func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) { return filter, resp, err } -// GetMyFilters retrieves the my Filters. +// Get wraps GetWithContext using the background context. +func (fs *FilterService) Get(filterID int) (*Filter, *Response, error) { + return fs.GetWithContext(context.Background(), filterID) +} + +// GetMyFiltersWithContext retrieves the my Filters. // // https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-my-get -func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) { +func (fs *FilterService) GetMyFiltersWithContext(ctx context.Context, opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) { apiEndpoint := "rest/api/3/filter/my" url, err := addOptions(apiEndpoint, opts) if err != nil { return nil, nil, err } - req, err := fs.client.NewRequest("GET", url, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err } @@ -202,16 +218,21 @@ func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter return filters, resp, nil } -// Search will search for filter according to the search options +// GetMyFilters wraps GetMyFiltersWithContext using the background context. +func (fs *FilterService) GetMyFilters(opts *GetMyFiltersQueryOptions) ([]*Filter, *Response, error) { + return fs.GetMyFiltersWithContext(context.Background(), opts) +} + +// SearchWithContext will search for filter according to the search options // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-rest-api-3-filter-search-get -func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Response, error) { +func (fs *FilterService) SearchWithContext(ctx context.Context, opt *FilterSearchOptions) (*FiltersList, *Response, error) { apiEndpoint := "rest/api/3/filter/search" url, err := addOptions(apiEndpoint, opt) if err != nil { return nil, nil, err } - req, err := fs.client.NewRequest("GET", url, nil) + req, err := fs.client.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, nil, err } @@ -225,3 +246,8 @@ func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Respon return filters, resp, err } + +// Search wraps SearchWithContext using the background context. +func (fs *FilterService) Search(opt *FilterSearchOptions) (*FiltersList, *Response, error) { + return fs.SearchWithContext(context.Background(), opt) +} diff --git a/filter_test.go b/filter_test.go index 9df3304c..1b3a6903 100644 --- a/filter_test.go +++ b/filter_test.go @@ -41,7 +41,7 @@ func TestFilterService_Get(t *testing.T) { testMux.HandleFunc(testAPIEndpoint, func(writer http.ResponseWriter, request *http.Request) { testMethod(t, request, "GET") testRequestURL(t, request, testAPIEndpoint) - fmt.Fprintf(writer, string(raw)) + fmt.Fprint(writer, string(raw)) }) filter, _, err := testClient.Filter.Get(10000) diff --git a/group.go b/group.go index 018ba699..9f67105f 100644 --- a/group.go +++ b/group.go @@ -1,6 +1,7 @@ package jira import ( + "context" "fmt" "net/url" ) @@ -58,16 +59,16 @@ type GroupSearchOptions struct { IncludeInactiveUsers bool } -// Get returns a paginated list of users who are members of the specified group and its subgroups. +// GetWithContext returns a paginated list of users who are members of the specified group and its subgroups. // Users in the page are ordered by user names. // User of this resource is required to have sysadmin or admin permissions. // // Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup // // WARNING: This API only returns the first page of group members -func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { +func (s *GroupService) GetWithContext(ctx context.Context, name string) ([]GroupMember, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -81,12 +82,17 @@ func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { return group.Members, resp, nil } -// GetWithOptions returns a paginated list of members of the specified group and its subgroups. +// Get wraps GetWithContext using the background context. +func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { + return s.GetWithContext(context.Background(), name) +} + +// GetWithOptionsWithContext returns a paginated list of members of the specified group and its subgroups. // Users in the page are ordered by user names. // User of this resource is required to have sysadmin or admin permissions. // // Jira API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup -func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { +func (s *GroupService) GetWithOptionsWithContext(ctx context.Context, name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { var apiEndpoint string if options == nil { apiEndpoint = fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) @@ -99,7 +105,7 @@ func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) options.IncludeInactiveUsers, ) } - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -112,16 +118,21 @@ func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) return group.Members, resp, nil } -// Add adds user to group +// GetWithOptions wraps GetWithOptionsWithContext using the background context. +func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { + return s.GetWithOptionsWithContext(context.Background(), name, options) +} + +// AddWithContext adds user to group // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup -func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) { +func (s *GroupService) AddWithContext(ctx context.Context, groupname string, username string) (*Group, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s", groupname) var user struct { Name string `json:"name"` } user.Name = username - req, err := s.client.NewRequest("POST", apiEndpoint, &user) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, &user) if err != nil { return nil, nil, err } @@ -136,12 +147,17 @@ func (s *GroupService) Add(groupname string, username string) (*Group, *Response return responseGroup, resp, nil } -// Remove removes user from group +// Add wraps AddWithContext using the background context. +func (s *GroupService) Add(groupname string, username string) (*Group, *Response, error) { + return s.AddWithContext(context.Background(), groupname, username) +} + +// RemoveWithContext removes user from group // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-removeUserFromGroup -func (s *GroupService) Remove(groupname string, username string) (*Response, error) { +func (s *GroupService) RemoveWithContext(ctx context.Context, groupname string, username string) (*Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/group/user?groupname=%s&username=%s", groupname, username) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, err } @@ -154,3 +170,8 @@ func (s *GroupService) Remove(groupname string, username string) (*Response, err return resp, nil } + +// Remove wraps RemoveWithContext using the background context. +func (s *GroupService) Remove(groupname string, username string) (*Response, error) { + return s.RemoveWithContext(context.Background(), groupname, username) +} diff --git a/issue.go b/issue.go index fac0387d..9cd46407 100644 --- a/issue.go +++ b/issue.go @@ -2,6 +2,7 @@ package jira import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -10,6 +11,7 @@ import ( "net/http" "net/url" "reflect" + "strconv" "strings" "time" @@ -47,6 +49,7 @@ type Issue struct { RenderedFields *IssueRenderedFields `json:"renderedFields,omitempty" structs:"renderedFields,omitempty"` Changelog *Changelog `json:"changelog,omitempty" structs:"changelog,omitempty"` Transitions []Transition `json:"transitions,omitempty" structs:"transitions,omitempty"` + Names map[string]string `json:"names,omitempty" structs:"names,omitempty"` } // ChangelogItems reflects one single changelog item of a history item @@ -191,7 +194,7 @@ func (i *IssueFields) UnmarshalJSON(data []byte) error { options := strings.Split(tagDetail, ",") if len(options) == 0 { - return fmt.Errorf("No tags options found for %s", field.Name) + return fmt.Errorf("no tags options found for %s", field.Name) } // the first one is the json tag key := options[0] @@ -586,7 +589,7 @@ type RemoteLinkStatus struct { Icon *RemoteLinkIcon } -// Get returns a full representation of the issue for the given issue key. +// GetWithContext returns a full representation of the issue for the given issue key. // Jira will attempt to identify the issue by the issueIdOrKey path parameter. // This can be an issue id, or an issue key. // If the issue cannot be found via an exact match, Jira will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. @@ -594,9 +597,9 @@ type RemoteLinkStatus struct { // The given options will be appended to the query string // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue -func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { +func (s *IssueService) GetWithContext(ctx context.Context, issueID string, options *GetQueryOptions) (*Issue, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -619,13 +622,18 @@ func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *R return issue, resp, nil } -// DownloadAttachment returns a Response of an attachment for a given attachmentID. +// Get wraps GetWithContext using the background context. +func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { + return s.GetWithContext(context.Background(), issueID, options) +} + +// DownloadAttachmentWithContext returns a Response of an attachment for a given attachmentID. // The attachment is in the Response.Body of the response. // This is an io.ReadCloser. // The caller should close the resp.Body. -func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) { +func (s *IssueService) DownloadAttachmentWithContext(ctx context.Context, attachmentID string) (*Response, error) { apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, err } @@ -639,8 +647,13 @@ func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error return resp, nil } -// PostAttachment uploads r (io.Reader) as an attachment to a given issueID -func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) { +// DownloadAttachment wraps DownloadAttachmentWithContext using the background context. +func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error) { + return s.DownloadAttachmentWithContext(context.Background(), attachmentID) +} + +// PostAttachmentWithContext uploads r (io.Reader) as an attachment to a given issueID +func (s *IssueService) PostAttachmentWithContext(ctx context.Context, issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", issueID) b := new(bytes.Buffer) @@ -659,7 +672,7 @@ func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentNam } writer.Close() - req, err := s.client.NewMultiPartRequest("POST", apiEndpoint, b) + req, err := s.client.NewMultiPartRequestWithContext(ctx, "POST", apiEndpoint, b) if err != nil { return nil, nil, err } @@ -677,11 +690,16 @@ func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentNam return attachment, resp, nil } -// DeleteAttachment deletes an attachment of a given attachmentID -func (s *IssueService) DeleteAttachment(attachmentID string) (*Response, error) { +// PostAttachment wraps PostAttachmentWithContext using the background context. +func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) { + return s.PostAttachmentWithContext(context.Background(), issueID, r, attachmentName) +} + +// DeleteAttachmentWithContext deletes an attachment of a given attachmentID +func (s *IssueService) DeleteAttachmentWithContext(ctx context.Context, attachmentID string) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/attachment/%s", attachmentID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, err } @@ -695,14 +713,19 @@ func (s *IssueService) DeleteAttachment(attachmentID string) (*Response, error) return resp, nil } -// GetWorklogs gets all the worklogs for an issue. +// DeleteAttachment wraps DeleteAttachmentWithContext using the background context. +func (s *IssueService) DeleteAttachment(attachmentID string) (*Response, error) { + return s.DeleteAttachmentWithContext(context.Background(), attachmentID) +} + +// GetWorklogsWithContext gets all the worklogs for an issue. // This method is especially important if you need to read all the worklogs, not just the first page. // // https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-getIssueWorklog -func (s *IssueService) GetWorklogs(issueID string, options ...func(*http.Request) error) (*Worklog, *Response, error) { +func (s *IssueService) GetWorklogsWithContext(ctx context.Context, issueID string, options ...func(*http.Request) error) (*Worklog, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -719,6 +742,11 @@ func (s *IssueService) GetWorklogs(issueID string, options ...func(*http.Request return v, resp, err } +// GetWorklogs wraps GetWorklogsWithContext using the background context. +func (s *IssueService) GetWorklogs(issueID string, options ...func(*http.Request) error) (*Worklog, *Response, error) { + return s.GetWorklogsWithContext(context.Background(), issueID, options...) +} + // Applies query options to http request. // This helper is meant to be used with all "QueryOptions" structs. func WithQueryOptions(options interface{}) func(*http.Request) error { @@ -735,14 +763,14 @@ func WithQueryOptions(options interface{}) func(*http.Request) error { } } -// Create creates an issue or a sub-task from a JSON representation. +// CreateWithContext creates an issue or a sub-task from a JSON representation. // Creating a sub-task is similar to creating a regular issue, with two important differences: // The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues -func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) { +func (s *IssueService) CreateWithContext(ctx context.Context, issue *Issue) (*Issue, *Response, error) { apiEndpoint := "rest/api/2/issue" - req, err := s.client.NewRequest("POST", apiEndpoint, issue) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, issue) if err != nil { return nil, nil, err } @@ -756,26 +784,31 @@ func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) { defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, resp, fmt.Errorf("Could not read the returned data") + return nil, resp, fmt.Errorf("could not read the returned data") } err = json.Unmarshal(data, responseIssue) if err != nil { - return nil, resp, fmt.Errorf("Could not unmarshall the data into struct") + return nil, resp, fmt.Errorf("could not unmarshall the data into struct") } return responseIssue, resp, nil } -// UpdateWithOptions updates an issue from a JSON representation, -// while also specifiying query params. The issue is found by key. +// Create wraps CreateWithContext using the background context. +func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) { + return s.CreateWithContext(context.Background(), issue) +} + +// UpdateWithOptionsWithContext updates an issue from a JSON representation, +// while also specifying query params. The issue is found by key. // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue -func (s *IssueService) UpdateWithOptions(issue *Issue, opts *UpdateQueryOptions) (*Issue, *Response, error) { +func (s *IssueService) UpdateWithOptionsWithContext(ctx context.Context, issue *Issue, opts *UpdateQueryOptions) (*Issue, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", issue.Key) url, err := addOptions(apiEndpoint, opts) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest("PUT", url, issue) + req, err := s.client.NewRequestWithContext(ctx, "PUT", url, issue) if err != nil { return nil, nil, err } @@ -791,19 +824,29 @@ func (s *IssueService) UpdateWithOptions(issue *Issue, opts *UpdateQueryOptions) return &ret, resp, nil } -// Update updates an issue from a JSON representation. The issue is found by key. +// UpdateWithOptions wraps UpdateWithOptionsWithContext using the background context. +func (s *IssueService) UpdateWithOptions(issue *Issue, opts *UpdateQueryOptions) (*Issue, *Response, error) { + return s.UpdateWithOptionsWithContext(context.Background(), issue, opts) +} + +// UpdateWithContext updates an issue from a JSON representation. The issue is found by key. // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue -func (s *IssueService) Update(issue *Issue) (*Issue, *Response, error) { +func (s *IssueService) UpdateWithContext(ctx context.Context, issue *Issue) (*Issue, *Response, error) { return s.UpdateWithOptions(issue, nil) } -// UpdateIssue updates an issue from a JSON representation. The issue is found by key. +// Update wraps UpdateWithContext using the background context. +func (s *IssueService) Update(issue *Issue) (*Issue, *Response, error) { + return s.UpdateWithContext(context.Background(), issue) +} + +// UpdateIssueWithContext updates an issue from a JSON representation. The issue is found by key. // // https://docs.atlassian.com/jira/REST/7.4.0/#api/2/issue-editIssue -func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) (*Response, error) { +func (s *IssueService) UpdateIssueWithContext(ctx context.Context, jiraID string, data map[string]interface{}) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", jiraID) - req, err := s.client.NewRequest("PUT", apiEndpoint, data) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, data) if err != nil { return nil, err } @@ -817,12 +860,17 @@ func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) ( return resp, nil } -// AddComment adds a new comment to issueID. +// UpdateIssue wraps UpdateIssueWithContext using the background context. +func (s *IssueService) UpdateIssue(jiraID string, data map[string]interface{}) (*Response, error) { + return s.UpdateIssueWithContext(context.Background(), jiraID, data) +} + +// AddCommentWithContext adds a new comment to issueID. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment -func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) { +func (s *IssueService) AddCommentWithContext(ctx context.Context, issueID string, comment *Comment) (*Comment, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment", issueID) - req, err := s.client.NewRequest("POST", apiEndpoint, comment) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, comment) if err != nil { return nil, nil, err } @@ -837,17 +885,22 @@ func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, * return responseComment, resp, nil } -// UpdateComment updates the body of a comment, identified by comment.ID, on the issueID. +// AddComment wraps AddCommentWithContext using the background context. +func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) { + return s.AddCommentWithContext(context.Background(), issueID, comment) +} + +// UpdateCommentWithContext updates the body of a comment, identified by comment.ID, on the issueID. // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-updateComment -func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment, *Response, error) { +func (s *IssueService) UpdateCommentWithContext(ctx context.Context, issueID string, comment *Comment) (*Comment, *Response, error) { reqBody := struct { Body string `json:"body"` }{ Body: comment.Body, } apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, comment.ID) - req, err := s.client.NewRequest("PUT", apiEndpoint, reqBody) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, reqBody) if err != nil { return nil, nil, err } @@ -861,12 +914,17 @@ func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment return responseComment, resp, nil } -// DeleteComment Deletes a comment from an issueID. +// UpdateComment wraps UpdateCommentWithContext using the background context. +func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment, *Response, error) { + return s.UpdateCommentWithContext(context.Background(), issueID, comment) +} + +// DeleteCommentWithContext Deletes a comment from an issueID. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-issue-issueIdOrKey-comment-id-delete -func (s *IssueService) DeleteComment(issueID, commentID string) error { +func (s *IssueService) DeleteCommentWithContext(ctx context.Context, issueID, commentID string) error { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, commentID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return err } @@ -880,12 +938,17 @@ func (s *IssueService) DeleteComment(issueID, commentID string) error { return nil } -// AddWorklogRecord adds a new worklog record to issueID. +// DeleteComment wraps DeleteCommentWithContext using the background context. +func (s *IssueService) DeleteComment(issueID, commentID string) error { + return s.DeleteCommentWithContext(context.Background(), issueID, commentID) +} + +// AddWorklogRecordWithContext adds a new worklog record to issueID. // // https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-issue-issueIdOrKey-worklog-post -func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { +func (s *IssueService) AddWorklogRecordWithContext(ctx context.Context, issueID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID) - req, err := s.client.NewRequest("POST", apiEndpoint, record) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, record) if err != nil { return nil, nil, err } @@ -907,12 +970,17 @@ func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord, o return responseRecord, resp, nil } -// UpdateWorklogRecord updates a worklog record. +// AddWorklogRecord wraps AddWorklogRecordWithContext using the background context. +func (s *IssueService) AddWorklogRecord(issueID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { + return s.AddWorklogRecordWithContext(context.Background(), issueID, record, options...) +} + +// UpdateWorklogRecordWithContext updates a worklog record. // // https://docs.atlassian.com/software/jira/docs/api/REST/7.1.2/#api/2/issue-updateWorklog -func (s *IssueService) UpdateWorklogRecord(issueID, worklogID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { +func (s *IssueService) UpdateWorklogRecordWithContext(ctx context.Context, issueID, worklogID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog/%s", issueID, worklogID) - req, err := s.client.NewRequest("PUT", apiEndpoint, record) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, record) if err != nil { return nil, nil, err } @@ -934,12 +1002,17 @@ func (s *IssueService) UpdateWorklogRecord(issueID, worklogID string, record *Wo return responseRecord, resp, nil } -// AddLink adds a link between two issues. +// UpdateWorklogRecord wraps UpdateWorklogRecordWithContext using the background context. +func (s *IssueService) UpdateWorklogRecord(issueID, worklogID string, record *WorklogRecord, options ...func(*http.Request) error) (*WorklogRecord, *Response, error) { + return s.UpdateWorklogRecordWithContext(context.Background(), issueID, worklogID, record, options...) +} + +// AddLinkWithContext adds a link between two issues. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink -func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) { - apiEndpoint := fmt.Sprintf("rest/api/2/issueLink") - req, err := s.client.NewRequest("POST", apiEndpoint, issueLink) +func (s *IssueService) AddLinkWithContext(ctx context.Context, issueLink *IssueLink) (*Response, error) { + apiEndpoint := "rest/api/2/issueLink" + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, issueLink) if err != nil { return nil, err } @@ -952,33 +1025,44 @@ func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) { return resp, err } -// Search will search for tickets according to the jql +// AddLink wraps AddLinkWithContext using the background context. +func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) { + return s.AddLinkWithContext(context.Background(), issueLink) +} + +// SearchWithContext will search for tickets according to the jql // // Jira API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues -func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) { - var u string - if options == nil { - u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql)) - } else { - u = "rest/api/2/search?jql=" + url.QueryEscape(jql) +func (s *IssueService) SearchWithContext(ctx context.Context, jql string, options *SearchOptions) ([]Issue, *Response, error) { + u := url.URL{ + Path: "rest/api/2/search", + } + uv := url.Values{} + if jql != "" { + uv.Add("jql", jql) + } + + if options != nil { if options.StartAt != 0 { - u += fmt.Sprintf("&startAt=%d", options.StartAt) + uv.Add("startAt", strconv.Itoa(options.StartAt)) } if options.MaxResults != 0 { - u += fmt.Sprintf("&maxResults=%d", options.MaxResults) + uv.Add("maxResults", strconv.Itoa(options.MaxResults)) } if options.Expand != "" { - u += fmt.Sprintf("&expand=%s", options.Expand) + uv.Add("expand", options.Expand) } if strings.Join(options.Fields, ",") != "" { - u += fmt.Sprintf("&fields=%s", strings.Join(options.Fields, ",")) + uv.Add("fields", strings.Join(options.Fields, ",")) } if options.ValidateQuery != "" { - u += fmt.Sprintf("&validateQuery=%s", options.ValidateQuery) + uv.Add("validateQuery", options.ValidateQuery) } } - req, err := s.client.NewRequest("GET", u, nil) + u.RawQuery = uv.Encode() + + req, err := s.client.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { return []Issue{}, nil, err } @@ -991,10 +1075,15 @@ func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Res return v.Issues, resp, err } -// SearchPages will get issues from all pages in a search +// Search wraps SearchWithContext using the background context. +func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) { + return s.SearchWithContext(context.Background(), jql, options) +} + +// SearchPagesWithContext will get issues from all pages in a search // // Jira API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues -func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Issue) error) error { +func (s *IssueService) SearchPagesWithContext(ctx context.Context, jql string, options *SearchOptions, f func(Issue) error) error { if options == nil { options = &SearchOptions{ StartAt: 0, @@ -1035,10 +1124,15 @@ func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Is } } -// GetCustomFields returns a map of customfield_* keys with string values -func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) { +// SearchPages wraps SearchPagesWithContext using the background context. +func (s *IssueService) SearchPages(jql string, options *SearchOptions, f func(Issue) error) error { + return s.SearchPagesWithContext(context.Background(), jql, options, f) +} + +// GetCustomFieldsWithContext returns a map of customfield_* keys with string values +func (s *IssueService) GetCustomFieldsWithContext(ctx context.Context, issueID string) (CustomFields, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -1072,13 +1166,18 @@ func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, return cf, resp, nil } -// GetTransitions gets a list of the transitions possible for this issue by the current user, +// GetCustomFields wraps GetCustomFieldsWithContext using the background context. +func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) { + return s.GetCustomFieldsWithContext(context.Background(), issueID) +} + +// GetTransitionsWithContext gets a list of the transitions possible for this issue by the current user, // along with fields that are required and their types. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions -func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) { +func (s *IssueService) GetTransitionsWithContext(ctx context.Context, id string) ([]Transition, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions?expand=transitions.fields", id) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -1091,27 +1190,37 @@ func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error return result.Transitions, resp, err } -// DoTransition performs a transition on an issue. +// GetTransitions wraps GetTransitionsWithContext using the background context. +func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) { + return s.GetTransitionsWithContext(context.Background(), id) +} + +// DoTransitionWithContext performs a transition on an issue. // When performing the transition you can update or set other issue fields. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition -func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) { +func (s *IssueService) DoTransitionWithContext(ctx context.Context, ticketID, transitionID string) (*Response, error) { payload := CreateTransitionPayload{ Transition: TransitionPayload{ ID: transitionID, }, } - return s.DoTransitionWithPayload(ticketID, payload) + return s.DoTransitionWithPayloadWithContext(ctx, ticketID, payload) +} + +// DoTransition wraps DoTransitionWithContext using the background context. +func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) { + return s.DoTransitionWithContext(context.Background(), ticketID, transitionID) } -// DoTransitionWithPayload performs a transition on an issue using any payload. +// DoTransitionWithPayloadWithContext performs a transition on an issue using any payload. // When performing the transition you can update or set other issue fields. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition -func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (*Response, error) { +func (s *IssueService) DoTransitionWithPayloadWithContext(ctx context.Context, ticketID, payload interface{}) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID) - req, err := s.client.NewRequest("POST", apiEndpoint, payload) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload) if err != nil { return nil, err } @@ -1124,6 +1233,11 @@ func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (* return resp, err } +// DoTransitionWithPayload wraps DoTransitionWithPayloadWithContext using the background context. +func (s *IssueService) DoTransitionWithPayload(ticketID, payload interface{}) (*Response, error) { + return s.DoTransitionWithPayloadWithContext(context.Background(), ticketID, payload) +} + // InitIssueWithMetaAndFields returns Issue with with values from fieldsConfig properly set. // * metaProject should contain metaInformation about the project where the issue should be created. // * metaIssuetype is the MetaInformation about the Issuetype that needs to be created. @@ -1194,7 +1308,7 @@ func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIss Value: value, } default: - return nil, fmt.Errorf("Unknown issue type encountered: %s for %s", valueType, key) + return nil, fmt.Errorf("unknown issue type encountered: %s for %s", valueType, key) } } @@ -1203,8 +1317,8 @@ func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIss return issue, nil } -// Delete will delete a specified issue. -func (s *IssueService) Delete(issueID string) (*Response, error) { +// DeleteWithContext will delete a specified issue. +func (s *IssueService) DeleteWithContext(ctx context.Context, issueID string) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID) // to enable deletion of subtasks; without this, the request will fail if the issue has subtasks @@ -1212,7 +1326,7 @@ func (s *IssueService) Delete(issueID string) (*Response, error) { deletePayload["deleteSubtasks"] = "true" content, _ := json.Marshal(deletePayload) - req, err := s.client.NewRequest("DELETE", apiEndpoint, content) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, content) if err != nil { return nil, err } @@ -1221,13 +1335,18 @@ func (s *IssueService) Delete(issueID string) (*Response, error) { return resp, err } -// GetWatchers wil return all the users watching/observing the given issue +// Delete wraps DeleteWithContext using the background context. +func (s *IssueService) Delete(issueID string) (*Response, error) { + return s.DeleteWithContext(context.Background(), issueID) +} + +// GetWatchersWithContext wil return all the users watching/observing the given issue // // Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-getIssueWatchers -func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { +func (s *IssueService) GetWatchersWithContext(ctx context.Context, issueID string) (*[]User, *Response, error) { watchesAPIEndpoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - req, err := s.client.NewRequest("GET", watchesAPIEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", watchesAPIEndpoint, nil) if err != nil { return nil, nil, err } @@ -1239,8 +1358,8 @@ func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { } result := []User{} - user := new(User) for _, watcher := range watches.Watchers { + var user *User if watcher.AccountID != "" { user, resp, err = s.client.User.GetByAccountID(watcher.AccountID) if err != nil { @@ -1259,13 +1378,18 @@ func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { return &result, resp, nil } -// AddWatcher adds watcher to the given issue +// GetWatchers wraps GetWatchersWithContext using the background context. +func (s *IssueService) GetWatchers(issueID string) (*[]User, *Response, error) { + return s.GetWatchersWithContext(context.Background(), issueID) +} + +// AddWatcherWithContext adds watcher to the given issue // // Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-addWatcher -func (s *IssueService) AddWatcher(issueID string, userName string) (*Response, error) { +func (s *IssueService) AddWatcherWithContext(ctx context.Context, issueID string, userName string) (*Response, error) { apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - req, err := s.client.NewRequest("POST", apiEndPoint, userName) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndPoint, userName) if err != nil { return nil, err } @@ -1278,13 +1402,18 @@ func (s *IssueService) AddWatcher(issueID string, userName string) (*Response, e return resp, err } -// RemoveWatcher removes given user from given issue +// AddWatcher wraps AddWatcherWithContext using the background context. +func (s *IssueService) AddWatcher(issueID string, userName string) (*Response, error) { + return s.AddWatcherWithContext(context.Background(), issueID, userName) +} + +// RemoveWatcherWithContext removes given user from given issue // // Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/latest/#api/2/issue-removeWatcher -func (s *IssueService) RemoveWatcher(issueID string, userName string) (*Response, error) { +func (s *IssueService) RemoveWatcherWithContext(ctx context.Context, issueID string, userName string) (*Response, error) { apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/watchers", issueID) - req, err := s.client.NewRequest("DELETE", apiEndPoint, userName) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndPoint, userName) if err != nil { return nil, err } @@ -1297,13 +1426,18 @@ func (s *IssueService) RemoveWatcher(issueID string, userName string) (*Response return resp, err } -// UpdateAssignee updates the user assigned to work on the given issue +// RemoveWatcher wraps RemoveWatcherWithContext using the background context. +func (s *IssueService) RemoveWatcher(issueID string, userName string) (*Response, error) { + return s.RemoveWatcherWithContext(context.Background(), issueID, userName) +} + +// UpdateAssigneeWithContext updates the user assigned to work on the given issue // // Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/7.10.2/#api/2/issue-assign -func (s *IssueService) UpdateAssignee(issueID string, assignee *User) (*Response, error) { +func (s *IssueService) UpdateAssigneeWithContext(ctx context.Context, issueID string, assignee *User) (*Response, error) { apiEndPoint := fmt.Sprintf("rest/api/2/issue/%s/assignee", issueID) - req, err := s.client.NewRequest("PUT", apiEndPoint, assignee) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndPoint, assignee) if err != nil { return nil, err } @@ -1316,6 +1450,11 @@ func (s *IssueService) UpdateAssignee(issueID string, assignee *User) (*Response return resp, err } +// UpdateAssignee wraps UpdateAssigneeWithContext using the background context. +func (s *IssueService) UpdateAssignee(issueID string, assignee *User) (*Response, error) { + return s.UpdateAssigneeWithContext(context.Background(), issueID, assignee) +} + func (c ChangelogHistory) CreatedTime() (time.Time, error) { var t time.Time // Ignore null @@ -1326,12 +1465,12 @@ func (c ChangelogHistory) CreatedTime() (time.Time, error) { return t, err } -// GetRemoteLinks gets remote issue links on the issue. +// GetRemoteLinksWithContext gets remote issue links on the issue. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getRemoteIssueLinks -func (s *IssueService) GetRemoteLinks(id string) (*[]RemoteLink, *Response, error) { +func (s *IssueService) GetRemoteLinksWithContext(ctx context.Context, id string) (*[]RemoteLink, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/remotelink", id) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -1344,12 +1483,17 @@ func (s *IssueService) GetRemoteLinks(id string) (*[]RemoteLink, *Response, erro return result, resp, err } -// AddRemoteLink adds a remote link to issueID. +// GetRemoteLinks wraps GetRemoteLinksWithContext using the background context. +func (s *IssueService) GetRemoteLinks(id string) (*[]RemoteLink, *Response, error) { + return s.GetRemoteLinksWithContext(context.Background(), id) +} + +// AddRemoteLinkWithContext adds a remote link to issueID. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issue-issueIdOrKey-remotelink-post -func (s *IssueService) AddRemoteLink(issueID string, remotelink *RemoteLink) (*RemoteLink, *Response, error) { +func (s *IssueService) AddRemoteLinkWithContext(ctx context.Context, issueID string, remotelink *RemoteLink) (*RemoteLink, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/remotelink", issueID) - req, err := s.client.NewRequest("POST", apiEndpoint, remotelink) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, remotelink) if err != nil { return nil, nil, err } @@ -1363,3 +1507,8 @@ func (s *IssueService) AddRemoteLink(issueID string, remotelink *RemoteLink) (*R return responseRemotelink, resp, nil } + +// AddRemoteLink wraps AddRemoteLinkWithContext using the background context. +func (s *IssueService) AddRemoteLink(issueID string, remotelink *RemoteLink) (*RemoteLink, *Response, error) { + return s.AddRemoteLinkWithContext(context.Background(), issueID, remotelink) +} diff --git a/issue_test.go b/issue_test.go index fa3f4551..bc8b3630 100644 --- a/issue_test.go +++ b/issue_test.go @@ -322,15 +322,16 @@ func TestIssueService_AddLink(t *testing.T) { }, } resp, err := testClient.Issue.AddLink(il) + if err != nil { + t.Errorf("Error given: %s", err) + } if resp == nil { t.Error("Expected response. Response is nil") + return } if resp.StatusCode != 200 { t.Errorf("Expected Status code 200. Given %d", resp.StatusCode) } - if err != nil { - t.Errorf("Error given: %s", err) - } } func TestIssueService_Get_Fields(t *testing.T) { @@ -344,8 +345,12 @@ func TestIssueService_Get_Fields(t *testing.T) { }) issue, _, err := testClient.Issue.Get("10002", nil) + if err != nil { + t.Errorf("Error given: %s", err) + } if issue == nil { t.Error("Expected issue. Issue is nil") + return } if !reflect.DeepEqual(issue.Fields.Labels, []string{"test"}) { t.Error("Expected labels for the returned issue") @@ -357,10 +362,6 @@ func TestIssueService_Get_Fields(t *testing.T) { if issue.Fields.Epic == nil { t.Error("Epic expected but not found") } - - if err != nil { - t.Errorf("Error given: %s", err) - } } func TestIssueService_Get_RenderedFields(t *testing.T) { @@ -374,8 +375,12 @@ func TestIssueService_Get_RenderedFields(t *testing.T) { }) issue, _, err := testClient.Issue.Get("10002", nil) + if err != nil { + t.Errorf("Error given: %s", err) + } if issue == nil { t.Error("Expected issue. Issue is nil") + return } if issue.RenderedFields.Updated != "2 hours ago" { t.Error("Expected updated to equla '2 hours ago' for rendered field") @@ -388,10 +393,6 @@ func TestIssueService_Get_RenderedFields(t *testing.T) { if comment.Body != "This is HTML" { t.Errorf("Wrong comment body returned in RenderedField. Got %s", comment.Body) } - - if err != nil { - t.Errorf("Error given: %s", err) - } } func TestIssueService_DownloadAttachment(t *testing.T) { @@ -408,8 +409,12 @@ func TestIssueService_DownloadAttachment(t *testing.T) { }) resp, err := testClient.Issue.DownloadAttachment("10000") + if err != nil { + t.Errorf("Error given: %s", err) + } if resp == nil { t.Error("Expected response. Response is nil") + return } defer resp.Body.Close() @@ -424,9 +429,6 @@ func TestIssueService_DownloadAttachment(t *testing.T) { if resp.StatusCode != 200 { t.Errorf("Expected Status code 200. Given %d", resp.StatusCode) } - if err != nil { - t.Errorf("Error given: %s", err) - } } func TestIssueService_DownloadAttachment_BadStatus(t *testing.T) { @@ -443,6 +445,7 @@ func TestIssueService_DownloadAttachment_BadStatus(t *testing.T) { resp, err := testClient.Issue.DownloadAttachment("10000") if resp == nil { t.Error("Expected response. Response is nil") + return } defer resp.Body.Close() @@ -468,10 +471,11 @@ func TestIssueService_PostAttachment(t *testing.T) { if err != nil { status = http.StatusNotAcceptable } + defer file.Close() + if file == nil { status = http.StatusNoContent } else { - // Read the file into memory data, err := ioutil.ReadAll(file) if err != nil { @@ -480,11 +484,9 @@ func TestIssueService_PostAttachment(t *testing.T) { if string(data) != testAttachment { status = http.StatusNotAcceptable } - - w.WriteHeader(status) - fmt.Fprint(w, `[{"self":"http://jira/jira/rest/api/2/attachment/228924","id":"228924","filename":"example.jpg","author":{"self":"http://jira/jira/rest/api/2/user?username=test","name":"test","emailAddress":"test@test.com","avatarUrls":{"16x16":"http://jira/jira/secure/useravatar?size=small&avatarId=10082","48x48":"http://jira/jira/secure/useravatar?avatarId=10082"},"displayName":"Tester","active":true},"created":"2016-05-24T00:25:17.000-0700","size":32280,"mimeType":"image/jpeg","content":"http://jira/jira/secure/attachment/228924/example.jpg","thumbnail":"http://jira/jira/secure/thumbnail/228924/_thumb_228924.png"}]`) - file.Close() } + w.WriteHeader(status) + fmt.Fprint(w, `[{"self":"http://jira/jira/rest/api/2/attachment/228924","id":"228924","filename":"example.jpg","author":{"self":"http://jira/jira/rest/api/2/user?username=test","name":"test","emailAddress":"test@test.com","avatarUrls":{"16x16":"http://jira/jira/secure/useravatar?size=small&avatarId=10082","48x48":"http://jira/jira/secure/useravatar?avatarId=10082"},"displayName":"Tester","active":true},"created":"2016-05-24T00:25:17.000-0700","size":32280,"mimeType":"image/jpeg","content":"http://jira/jira/secure/attachment/228924/example.jpg","thumbnail":"http://jira/jira/secure/thumbnail/228924/_thumb_228924.png"}]`) }) reader := strings.NewReader(testAttachment) @@ -580,13 +582,10 @@ func TestIssueService_DeleteAttachment(t *testing.T) { if resp.StatusCode == 404 { t.Error("Attachment not found") } - } else { - t.Log("Attachment deleted") } + if err != nil { t.Errorf("Error given: %s", err) - } else { - t.Log("No error") } } @@ -595,13 +594,44 @@ func TestIssueService_Search(t *testing.T) { defer teardown() testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testRequestURL(t, r, "/rest/api/2/search?jql=something&startAt=1&maxResults=40&expand=foo") + testRequestURL(t, r, "/rest/api/2/search?expand=foo&jql=type+%3D+Bug+and+Status+NOT+IN+%28Resolved%29&maxResults=40&startAt=1") w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`) }) opt := &SearchOptions{StartAt: 1, MaxResults: 40, Expand: "foo"} - _, resp, err := testClient.Issue.Search("something", opt) + _, resp, err := testClient.Issue.Search("type = Bug and Status NOT IN (Resolved)", opt) + + if resp == nil { + t.Errorf("Response given: %+v", resp) + } + if err != nil { + t.Errorf("Error given: %s", err) + } + + if resp.StartAt != 1 { + t.Errorf("StartAt should populate with 1, %v given", resp.StartAt) + } + if resp.MaxResults != 40 { + t.Errorf("StartAt should populate with 40, %v given", resp.MaxResults) + } + if resp.Total != 6 { + t.Errorf("StartAt should populate with 6, %v given", resp.Total) + } +} + +func TestIssueService_SearchEmptyJQL(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/api/2/search?expand=foo&maxResults=40&startAt=1") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`) + }) + + opt := &SearchOptions{StartAt: 1, MaxResults: 40, Expand: "foo"} + _, resp, err := testClient.Issue.Search("", opt) if resp == nil { t.Errorf("Response given: %+v", resp) @@ -630,7 +660,6 @@ func TestIssueService_Search_WithoutPaging(t *testing.T) { w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"expand": "schema,names","startAt": 0,"maxResults": 50,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`) }) - _, resp, err := testClient.Issue.Search("something", nil) if resp == nil { @@ -656,15 +685,15 @@ func TestIssueService_SearchPages(t *testing.T) { defer teardown() testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - if r.URL.String() == "/rest/api/2/search?jql=something&startAt=1&maxResults=2&expand=foo&validateQuery=warn" { + if r.URL.String() == "/rest/api/2/search?expand=foo&jql=something&maxResults=2&startAt=1&validateQuery=warn" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 2,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`) return - } else if r.URL.String() == "/rest/api/2/search?jql=something&startAt=3&maxResults=2&expand=foo&validateQuery=warn" { + } else if r.URL.String() == "/rest/api/2/search?expand=foo&jql=something&maxResults=2&startAt=3&validateQuery=warn" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"expand": "schema,names","startAt": 3,"maxResults": 2,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`) return - } else if r.URL.String() == "/rest/api/2/search?jql=something&startAt=5&maxResults=2&expand=foo&validateQuery=warn" { + } else if r.URL.String() == "/rest/api/2/search?expand=foo&jql=something&maxResults=2&startAt=5&validateQuery=warn" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, `{"expand": "schema,names","startAt": 5,"maxResults": 2,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}}]}`) return @@ -694,7 +723,7 @@ func TestIssueService_SearchPages_EmptyResult(t *testing.T) { defer teardown() testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - if r.URL.String() == "/rest/api/2/search?jql=something&startAt=1&maxResults=50&expand=foo&validateQuery=warn" { + if r.URL.String() == "/rest/api/2/search?expand=foo&jql=something&maxResults=50&startAt=1&validateQuery=warn" { w.WriteHeader(http.StatusOK) // This is what Jira outputs when the &maxResult= issue occurs. It used to cause SearchPages to go into an endless loop. fmt.Fprint(w, `{"expand": "schema,names","startAt": 0,"maxResults": 0,"total": 6,"issues": []}`) @@ -1638,6 +1667,7 @@ func TestIssueService_Get_Fields_Changelog(t *testing.T) { issue, _, _ := testClient.Issue.Get("10002", &GetQueryOptions{Expand: "changelog"}) if issue == nil { t.Error("Expected issue. Issue is nil") + return } if len(issue.Changelog.Histories) != 1 { @@ -1668,6 +1698,7 @@ func TestIssueService_Get_Transitions(t *testing.T) { issue, _, _ := testClient.Issue.Get("10002", &GetQueryOptions{Expand: "transitions"}) if issue == nil { t.Error("Expected issue. Issue is nil") + return } if len(issue.Transitions) != 1 { @@ -1696,8 +1727,12 @@ func TestIssueService_Get_Fields_AffectsVersions(t *testing.T) { }) issue, _, err := testClient.Issue.Get("10002", nil) + if err != nil { + t.Errorf("Error given: %s", err) + } if issue == nil { t.Error("Expected issue. Issue is nil") + return } if !reflect.DeepEqual(issue.Fields.AffectsVersions, []*AffectsVersion{ { @@ -1712,10 +1747,6 @@ func TestIssueService_Get_Fields_AffectsVersions(t *testing.T) { }) { t.Error("Expected AffectsVersions for the returned issue") } - - if err != nil { - t.Errorf("Error given: %s", err) - } } func TestIssueService_GetRemoteLinks(t *testing.T) { @@ -1736,13 +1767,13 @@ func TestIssueService_GetRemoteLinks(t *testing.T) { }) remoteLinks, _, err := testClient.Issue.GetRemoteLinks("123") - if err != nil { t.Errorf("Got error: %v", err) } if remoteLinks == nil { t.Error("Expected remote links list. Got nil.") + return } if len(*remoteLinks) != 2 { diff --git a/issuelinktype.go b/issuelinktype.go index b1597371..92076055 100644 --- a/issuelinktype.go +++ b/issuelinktype.go @@ -1,6 +1,7 @@ package jira import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -13,12 +14,12 @@ type IssueLinkTypeService struct { client *Client } -// GetList gets all of the issue link types from Jira. +// GetListWithContext gets all of the issue link types from Jira. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-get -func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) { +func (s *IssueLinkTypeService) GetListWithContext(ctx context.Context) ([]IssueLinkType, *Response, error) { apiEndpoint := "rest/api/2/issueLinkType" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -31,12 +32,17 @@ func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) { return linkTypeList, resp, nil } -// Get gets info of a specific issue link type from Jira. +// GetList wraps GetListWithContext using the background context. +func (s *IssueLinkTypeService) GetList() ([]IssueLinkType, *Response, error) { + return s.GetListWithContext(context.Background()) +} + +// GetWithContext gets info of a specific issue link type from Jira. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-get -func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) { +func (s *IssueLinkTypeService) GetWithContext(ctx context.Context, ID string) (*IssueLinkType, *Response, error) { apiEndPoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", ID) - req, err := s.client.NewRequest("GET", apiEndPoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndPoint, nil) if err != nil { return nil, nil, err } @@ -49,12 +55,17 @@ func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) return linkType, resp, nil } -// Create creates an issue link type in Jira. +// Get wraps GetWithContext using the background context. +func (s *IssueLinkTypeService) Get(ID string) (*IssueLinkType, *Response, error) { + return s.GetWithContext(context.Background(), ID) +} + +// CreateWithContext creates an issue link type in Jira. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-post -func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, *Response, error) { +func (s *IssueLinkTypeService) CreateWithContext(ctx context.Context, linkType *IssueLinkType) (*IssueLinkType, *Response, error) { apiEndpoint := "/rest/api/2/issueLinkType" - req, err := s.client.NewRequest("POST", apiEndpoint, linkType) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, linkType) if err != nil { return nil, nil, err } @@ -68,23 +79,28 @@ func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - e := fmt.Errorf("Could not read the returned data") + e := fmt.Errorf("could not read the returned data") return nil, resp, NewJiraError(resp, e) } err = json.Unmarshal(data, responseLinkType) if err != nil { - e := fmt.Errorf("Could no unmarshal the data into struct") + e := fmt.Errorf("could no unmarshal the data into struct") return nil, resp, NewJiraError(resp, e) } return linkType, resp, nil } -// Update updates an issue link type. The issue is found by key. +// Create wraps CreateWithContext using the background context. +func (s *IssueLinkTypeService) Create(linkType *IssueLinkType) (*IssueLinkType, *Response, error) { + return s.CreateWithContext(context.Background(), linkType) +} + +// UpdateWithContext updates an issue link type. The issue is found by key. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-put -func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, *Response, error) { +func (s *IssueLinkTypeService) UpdateWithContext(ctx context.Context, linkType *IssueLinkType) (*IssueLinkType, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", linkType.ID) - req, err := s.client.NewRequest("PUT", apiEndpoint, linkType) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, linkType) if err != nil { return nil, nil, err } @@ -96,12 +112,17 @@ func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, return &ret, resp, nil } -// Delete deletes an issue link type based on provided ID. +// Update wraps UpdateWithContext using the background context. +func (s *IssueLinkTypeService) Update(linkType *IssueLinkType) (*IssueLinkType, *Response, error) { + return s.UpdateWithContext(context.Background(), linkType) +} + +// DeleteWithContext deletes an issue link type based on provided ID. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-issueLinkType-issueLinkTypeId-delete -func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) { +func (s *IssueLinkTypeService) DeleteWithContext(ctx context.Context, ID string) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/issueLinkType/%s", ID) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, err } @@ -109,3 +130,8 @@ func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) { resp, err := s.client.Do(req, nil) return resp, err } + +// Delete wraps DeleteWithContext using the background context. +func (s *IssueLinkTypeService) Delete(ID string) (*Response, error) { + return s.DeleteWithContext(context.Background(), ID) +} diff --git a/jira.go b/jira.go index 60966778..abca2015 100644 --- a/jira.go +++ b/jira.go @@ -2,6 +2,7 @@ package jira import ( "bytes" + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -105,10 +106,10 @@ func NewClient(httpClient httpClient, baseURL string) (*Client, error) { return c, nil } -// NewRawRequest creates an API request. +// NewRawRequestWithContext creates an API request. // A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. // Allows using an optional native io.Reader for sourcing the request body. -func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) { +func (c *Client) NewRawRequestWithContext(ctx context.Context, method, urlStr string, body io.Reader) (*http.Request, error) { rel, err := url.Parse(urlStr) if err != nil { return nil, err @@ -118,7 +119,7 @@ func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Req u := c.baseURL.ResolveReference(rel) - req, err := http.NewRequest(method, u.String(), body) + req, err := newRequestWithContext(ctx, method, u.String(), body) if err != nil { return nil, err } @@ -143,10 +144,15 @@ func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Req return req, nil } -// NewRequest creates an API request. +// NewRawRequest wraps NewRawRequestWithContext using the background context. +func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*http.Request, error) { + return c.NewRawRequestWithContext(context.Background(), method, urlStr, body) +} + +// NewRequestWithContext creates an API request. // A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. // If specified, the value pointed to by body is JSON encoded and included as the request body. -func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { +func (c *Client) NewRequestWithContext(ctx context.Context, method, urlStr string, body interface{}) (*http.Request, error) { rel, err := url.Parse(urlStr) if err != nil { return nil, err @@ -165,7 +171,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ } } - req, err := http.NewRequest(method, u.String(), buf) + req, err := newRequestWithContext(ctx, method, u.String(), buf) if err != nil { return nil, err } @@ -190,6 +196,11 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ return req, nil } +// NewRequest wraps NewRequestWithContext using the background context. +func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { + return c.NewRequestWithContext(context.Background(), method, urlStr, body) +} + // addOptions adds the parameters in opt as URL query parameters to s. opt // must be a struct whose fields may contain "url" tags. func addOptions(s string, opt interface{}) (string, error) { @@ -212,10 +223,10 @@ func addOptions(s string, opt interface{}) (string, error) { return u.String(), nil } -// NewMultiPartRequest creates an API request including a multi-part file. +// NewMultiPartRequestWithContext creates an API request including a multi-part file. // A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client. // If specified, the value pointed to by buf is a multipart form. -func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) { +func (c *Client) NewMultiPartRequestWithContext(ctx context.Context, method, urlStr string, buf *bytes.Buffer) (*http.Request, error) { rel, err := url.Parse(urlStr) if err != nil { return nil, err @@ -225,7 +236,7 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) ( u := c.baseURL.ResolveReference(rel) - req, err := http.NewRequest(method, u.String(), buf) + req, err := newRequestWithContext(ctx, method, u.String(), buf) if err != nil { return nil, err } @@ -251,6 +262,11 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) ( return req, nil } +// NewMultiPartRequest wraps NewMultiPartRequestWithContext using the background context. +func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) { + return c.NewMultiPartRequestWithContext(context.Background(), method, urlStr, buf) +} + // Do sends an API request and returns the API response. // The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred. func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { @@ -285,7 +301,7 @@ func CheckResponse(r *http.Response) error { return nil } - err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode) + err := fmt.Errorf("request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode) return err } @@ -324,7 +340,6 @@ func (r *Response) populatePageValues(v interface{}) { r.MaxResults = value.MaxResults r.Total = value.Total } - return } // BasicAuthTransport is an http.RoundTripper that authenticates all requests diff --git a/jira_test.go b/jira_test.go index a6341fec..9f34dd78 100644 --- a/jira_test.go +++ b/jira_test.go @@ -28,8 +28,6 @@ var ( testServer *httptest.Server ) -type testValues map[string]string - // setup sets up a test HTTP server along with a jira.Client that is configured to talk to that test server. // Tests should register handlers on mux which provide mock responses for the API method being tested. func setup() { @@ -88,13 +86,14 @@ func TestNewClient_WrongUrl(t *testing.T) { func TestNewClient_WithHttpClient(t *testing.T) { httpClient := http.DefaultClient httpClient.Timeout = 10 * time.Minute - c, err := NewClient(httpClient, testJiraInstanceURL) + c, err := NewClient(httpClient, testJiraInstanceURL) if err != nil { t.Errorf("Got an error: %s", err) } if c == nil { t.Error("Expected a client. Got none") + return } if !reflect.DeepEqual(c.client, httpClient) { t.Errorf("HTTP clients are not equal. Injected %+v, got %+v", httpClient, c.client) @@ -378,10 +377,6 @@ func TestClient_Do_HTTPResponse(t *testing.T) { setup() defer teardown() - type foo struct { - A string - } - testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if m := "GET"; m != r.Method { t.Errorf("Request method = %v, want %v", r.Method, m) diff --git a/metaissue.go b/metaissue.go index 19813786..560a2f05 100644 --- a/metaissue.go +++ b/metaissue.go @@ -1,6 +1,7 @@ package jira import ( + "context" "fmt" "strings" @@ -14,6 +15,11 @@ type CreateMetaInfo struct { Projects []*MetaProject `json:"projects,omitempty"` } +// EditMetaInfo contains information about fields and their attributed to edit a ticket. +type EditMetaInfo struct { + Fields tcontainer.MarshalMap `json:"fields,omitempty"` +} + // MetaProject is the meta information about a project returned from createmeta api type MetaProject struct { Expand string `json:"expand,omitempty"` @@ -42,16 +48,21 @@ type MetaIssueType struct { Fields tcontainer.MarshalMap `json:"fields,omitempty"` } -// GetCreateMeta makes the api call to get the meta information required to create a ticket +// GetCreateMetaWithContext makes the api call to get the meta information required to create a ticket +func (s *IssueService) GetCreateMetaWithContext(ctx context.Context, projectkeys string) (*CreateMetaInfo, *Response, error) { + return s.GetCreateMetaWithOptionsWithContext(ctx, &GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"}) +} + +// GetCreateMeta wraps GetCreateMetaWithContext using the background context. func (s *IssueService) GetCreateMeta(projectkeys string) (*CreateMetaInfo, *Response, error) { - return s.GetCreateMetaWithOptions(&GetQueryOptions{ProjectKeys: projectkeys, Expand: "projects.issuetypes.fields"}) + return s.GetCreateMetaWithContext(context.Background(), projectkeys) } -// GetCreateMetaWithOptions makes the api call to get the meta information without requiring to have a projectKey -func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) { +// GetCreateMetaWithOptionsWithContext makes the api call to get the meta information without requiring to have a projectKey +func (s *IssueService) GetCreateMetaWithOptionsWithContext(ctx context.Context, options *GetQueryOptions) (*CreateMetaInfo, *Response, error) { apiEndpoint := "rest/api/2/issue/createmeta" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -73,11 +84,40 @@ func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*Crea return meta, resp, nil } +// GetCreateMetaWithOptions wraps GetCreateMetaWithOptionsWithContext using the background context. +func (s *IssueService) GetCreateMetaWithOptions(options *GetQueryOptions) (*CreateMetaInfo, *Response, error) { + return s.GetCreateMetaWithOptionsWithContext(context.Background(), options) +} + +// GetEditMetaWithContext makes the api call to get the edit meta information for an issue +func (s *IssueService) GetEditMetaWithContext(ctx context.Context, issue *Issue) (*EditMetaInfo, *Response, error) { + apiEndpoint := fmt.Sprintf("/rest/api/2/issue/%s/editmeta", issue.Key) + + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + meta := new(EditMetaInfo) + resp, err := s.client.Do(req, meta) + + if err != nil { + return nil, resp, err + } + + return meta, resp, nil +} + +// GetEditMeta wraps GetEditMetaWithContext using the background context. +func (s *IssueService) GetEditMeta(issue *Issue) (*EditMetaInfo, *Response, error) { + return s.GetEditMetaWithContext(context.Background(), issue) +} + // GetProjectWithName returns a project with "name" from the meta information received. If not found, this returns nil. // The comparison of the name is case insensitive. func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject { for _, m := range m.Projects { - if strings.ToLower(m.Name) == strings.ToLower(name) { + if strings.EqualFold(m.Name, name) { return m } } @@ -88,7 +128,7 @@ func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject { // The comparison of the name is case insensitive. func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject { for _, m := range m.Projects { - if strings.ToLower(m.Key) == strings.ToLower(key) { + if strings.EqualFold(m.Key, key) { return m } } @@ -99,7 +139,7 @@ func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject { // The comparison of the name is case insensitive func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueType { for _, m := range p.IssueTypes { - if strings.ToLower(m.Name) == strings.ToLower(name) { + if strings.EqualFold(m.Name, name) { return m } } @@ -175,7 +215,7 @@ func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (boo for name := range mandatory { requiredFields = append(requiredFields, name) } - return false, fmt.Errorf("Required field not found in provided jira.fields. Required are: %#v", requiredFields) + return false, fmt.Errorf("required field not found in provided jira.fields. Required are: %#v", requiredFields) } } @@ -186,7 +226,7 @@ func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (boo for name := range all { availableFields = append(availableFields, name) } - return false, fmt.Errorf("Fields in jira.fields are not available in jira. Available are: %#v", availableFields) + return false, fmt.Errorf("fields in jira.fields are not available in jira. Available are: %#v", availableFields) } } diff --git a/metaissue_test.go b/metaissue_test.go index d19149b5..a77951a4 100644 --- a/metaissue_test.go +++ b/metaissue_test.go @@ -3,6 +3,7 @@ package jira import ( "fmt" "net/http" + "net/url" "testing" ) @@ -378,6 +379,83 @@ func TestIssueService_GetCreateMeta_Success(t *testing.T) { } +func TestIssueService_GetEditMeta_Success(t *testing.T) { + setup() + defer teardown() + + testAPIEndpoint := "/rest/api/2/issue/PROJ-9001/editmeta" + + testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, testAPIEndpoint) + + fmt.Fprint(w, `{ + "fields": { + "summary": { + "required": true, + "schema": { + "type": "string", + "system": "summary" + }, + "name": "Summary", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "attachment": { + "required": false, + "schema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "name": "Attachment", + "hasDefaultValue": false, + "operations": [ + + ] + } + } + }`) + }) + + editMeta, _, err := testClient.Issue.GetEditMeta(&Issue{Key: "PROJ-9001"}) + if err != nil { + t.Errorf("Expected nil error but got %s", err) + } + + requiredFields := 0 + fields := editMeta.Fields + for _, value := range fields { + for key, value := range value.(map[string]interface{}) { + if key == "required" && value == true { + requiredFields = requiredFields + 1 + } + } + + } + summary := fields["summary"].(map[string]interface{}) + attachment := fields["attachment"].(map[string]interface{}) + if summary["required"] != true { + t.Error("Expected summary to be required") + } + if attachment["required"] != false { + t.Error("Expected attachment to not be required") + } +} + +func TestIssueService_GetEditMeta_Fail(t *testing.T) { + _, _, err := testClient.Issue.GetEditMeta(&Issue{Key: "PROJ-9001"}) + if err == nil { + t.Error("Expected to receive an error, received nil instead") + } + + if _, ok := err.(*url.Error); !ok { + t.Errorf("Expected to receive an *url.Error, got %T instead", err) + } +} + func TestMetaIssueType_GetCreateMetaWithOptions(t *testing.T) { setup() defer teardown() diff --git a/permissionscheme.go b/permissionscheme.go index c8b684f2..7af5a8bf 100644 --- a/permissionscheme.go +++ b/permissionscheme.go @@ -1,6 +1,9 @@ package jira -import "fmt" +import ( + "context" + "fmt" +) // PermissionSchemeService handles permissionschemes for the Jira instance / API. // @@ -25,12 +28,12 @@ type Holder struct { Expand string `json:"expand" structs:"expand"` } -// GetList returns a list of all permission schemes +// GetListWithContext returns a list of all permission schemes // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-get -func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) { +func (s *PermissionSchemeService) GetListWithContext(ctx context.Context) (*PermissionSchemes, *Response, error) { apiEndpoint := "/rest/api/3/permissionscheme" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -45,12 +48,17 @@ func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, erro return pss, resp, nil } -// Get returns a full representation of the permission scheme for the schemeID +// GetList wraps GetListWithContext using the background context. +func (s *PermissionSchemeService) GetList() (*PermissionSchemes, *Response, error) { + return s.GetListWithContext(context.Background()) +} + +// GetWithContext returns a full representation of the permission scheme for the schemeID // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-permissionscheme-schemeId-get -func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) { +func (s *PermissionSchemeService) GetWithContext(ctx context.Context, schemeID int) (*PermissionScheme, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/3/permissionscheme/%d", schemeID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -62,8 +70,13 @@ func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Respons return nil, resp, jerr } if ps.Self == "" { - return nil, resp, fmt.Errorf("No permissionscheme with ID %d found", schemeID) + return nil, resp, fmt.Errorf("no permissionscheme with ID %d found", schemeID) } return ps, resp, nil } + +// Get wraps GetWithContext using the background context. +func (s *PermissionSchemeService) Get(schemeID int) (*PermissionScheme, *Response, error) { + return s.GetWithContext(context.Background(), schemeID) +} diff --git a/permissionschemes_test.go b/permissionschemes_test.go index 00a41b28..8efc1230 100644 --- a/permissionschemes_test.go +++ b/permissionschemes_test.go @@ -19,19 +19,20 @@ func TestPermissionSchemeService_GetList(t *testing.T) { testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") testRequestURL(t, r, testAPIEndpoint) - fmt.Fprintf(w, string(raw)) + fmt.Fprint(w, string(raw)) }) permissionScheme, _, err := testClient.PermissionScheme.GetList() + if err != nil { + t.Errorf("Error given: %v", err) + } if permissionScheme == nil { t.Error("Expected permissionScheme list. PermissionScheme list is nil") + return } if len(permissionScheme.PermissionSchemes) != 2 { t.Errorf("Expected %d permissionSchemes but got %d", 2, len(permissionScheme.PermissionSchemes)) } - if err != nil { - t.Errorf("Error given: %v", err) - } } func TestPermissionSchemeService_GetList_NoList(t *testing.T) { @@ -46,7 +47,7 @@ func TestPermissionSchemeService_GetList_NoList(t *testing.T) { testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") testRequestURL(t, r, testAPIEndpoint) - fmt.Fprintf(w, string(raw)) + fmt.Fprint(w, string(raw)) }) permissionScheme, _, err := testClient.PermissionScheme.GetList() @@ -69,7 +70,7 @@ func TestPermissionSchemeService_Get(t *testing.T) { testMux.HandleFunc(testAPIEdpoint, func(writer http.ResponseWriter, request *http.Request) { testMethod(t, request, "GET") testRequestURL(t, request, testAPIEdpoint) - fmt.Fprintf(writer, string(raw)) + fmt.Fprint(writer, string(raw)) }) permissionScheme, _, err := testClient.PermissionScheme.Get(10100) @@ -92,7 +93,7 @@ func TestPermissionSchemeService_Get_NoScheme(t *testing.T) { testMux.HandleFunc(testAPIEdpoint, func(writer http.ResponseWriter, request *http.Request) { testMethod(t, request, "GET") testRequestURL(t, request, testAPIEdpoint) - fmt.Fprintf(writer, string(raw)) + fmt.Fprint(writer, string(raw)) }) permissionScheme, _, err := testClient.PermissionScheme.Get(99999) diff --git a/priority.go b/priority.go index 1b452f12..a7b12a41 100644 --- a/priority.go +++ b/priority.go @@ -1,5 +1,7 @@ package jira +import "context" + // PriorityService handles priorities for the Jira instance / API. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Priority @@ -18,12 +20,12 @@ type Priority struct { Description string `json:"description,omitempty" structs:"description,omitempty"` } -// GetList gets all priorities from Jira +// GetListWithContext gets all priorities from Jira // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-priority-get -func (s *PriorityService) GetList() ([]Priority, *Response, error) { +func (s *PriorityService) GetListWithContext(ctx context.Context) ([]Priority, *Response, error) { apiEndpoint := "rest/api/2/priority" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -35,3 +37,8 @@ func (s *PriorityService) GetList() ([]Priority, *Response, error) { } return priorityList, resp, nil } + +// GetList wraps GetListWithContext using the background context. +func (s *PriorityService) GetList() ([]Priority, *Response, error) { + return s.GetListWithContext(context.Background()) +} diff --git a/project.go b/project.go index f9670b6d..f1000c81 100644 --- a/project.go +++ b/project.go @@ -1,6 +1,7 @@ package jira import ( + "context" "fmt" "github.com/google/go-querystring/query" @@ -80,20 +81,25 @@ type PermissionScheme struct { Permissions []Permission `json:"permissions" structs:"permissions,omitempty"` } -// GetList gets all projects form Jira +// GetListWithContext gets all projects form Jira // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects +func (s *ProjectService) GetListWithContext(ctx context.Context) (*ProjectList, *Response, error) { + return s.ListWithOptionsWithContext(ctx, &GetQueryOptions{}) +} + +// GetList wraps GetListWithContext using the background context. func (s *ProjectService) GetList() (*ProjectList, *Response, error) { - return s.ListWithOptions(&GetQueryOptions{}) + return s.GetListWithContext(context.Background()) } -// ListWithOptions gets all projects form Jira with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get +// ListWithOptionsWithContext gets all projects form Jira with optional query params, like &GetQueryOptions{Expand: "issueTypes"} to get // a list of all projects and their supported issuetypes // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects -func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) { +func (s *ProjectService) ListWithOptionsWithContext(ctx context.Context, options *GetQueryOptions) (*ProjectList, *Response, error) { apiEndpoint := "rest/api/2/project" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -116,14 +122,19 @@ func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList return projectList, resp, nil } -// Get returns a full representation of the project for the given issue key. +// ListWithOptions wraps ListWithOptionsWithContext using the background context. +func (s *ProjectService) ListWithOptions(options *GetQueryOptions) (*ProjectList, *Response, error) { + return s.ListWithOptionsWithContext(context.Background(), options) +} + +// GetWithContext returns a full representation of the project for the given issue key. // Jira will attempt to identify the project by the projectIdOrKey path parameter. // This can be an project id, or an project key. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject -func (s *ProjectService) Get(projectID string) (*Project, *Response, error) { +func (s *ProjectService) GetWithContext(ctx context.Context, projectID string) (*Project, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/project/%s", projectID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -138,14 +149,19 @@ func (s *ProjectService) Get(projectID string) (*Project, *Response, error) { return project, resp, nil } -// GetPermissionScheme returns a full representation of the permission scheme for the project +// Get wraps GetWithContext using the background context. +func (s *ProjectService) Get(projectID string) (*Project, *Response, error) { + return s.GetWithContext(context.Background(), projectID) +} + +// GetPermissionSchemeWithContext returns a full representation of the permission scheme for the project // Jira will attempt to identify the project by the projectIdOrKey path parameter. // This can be an project id, or an project key. // // Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject -func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) { +func (s *ProjectService) GetPermissionSchemeWithContext(ctx context.Context, projectID string) (*PermissionScheme, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s/permissionscheme", projectID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -159,3 +175,8 @@ func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionSchem return ps, resp, nil } + +// GetPermissionScheme wraps GetPermissionSchemeWithContext using the background context. +func (s *ProjectService) GetPermissionScheme(projectID string) (*PermissionScheme, *Response, error) { + return s.GetPermissionSchemeWithContext(context.Background(), projectID) +} diff --git a/project_test.go b/project_test.go index 897ba94b..6858b400 100644 --- a/project_test.go +++ b/project_test.go @@ -71,12 +71,13 @@ func TestProjectService_Get(t *testing.T) { }) projects, _, err := testClient.Project.Get("12310505") - if projects == nil { - t.Error("Expected project list. Project list is nil") - } if err != nil { t.Errorf("Error given: %s", err) } + if projects == nil { + t.Error("Expected project list. Project list is nil") + return + } if len(projects.Roles) != 9 { t.Errorf("Expected 9 roles but got %d", len(projects.Roles)) } diff --git a/request_context.go b/request_context.go new file mode 100644 index 00000000..fc0052df --- /dev/null +++ b/request_context.go @@ -0,0 +1,23 @@ +// +build go1.13 + +// This file provides glue to use Context in `http.Request` with +// Go version 1.13 and higher. + +// The function `http.NewRequestWithContext` has been added in Go 1.13. +// Before the release 1.13, to use Context we need creat `http.Request` +// then use the method `WithContext` to create a new `http.Request` +// with Context from the existing `http.Request`. +// +// Doc: https://golang.org/doc/go1.13#net/http + +package jira + +import ( + "context" + "io" + "net/http" +) + +func newRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) { + return http.NewRequestWithContext(ctx, method, url, body) +} diff --git a/request_legacy.go b/request_legacy.go new file mode 100644 index 00000000..5ceee886 --- /dev/null +++ b/request_legacy.go @@ -0,0 +1,28 @@ +// +build !go1.13 + +// This file provides glue to use Context in `http.Request` with +// Go version before 1.13. + +// The function `http.NewRequestWithContext` has been added in Go 1.13. +// Before the release 1.13, to use Context we need creat `http.Request` +// then use the method `WithContext` to create a new `http.Request` +// with Context from the existing `http.Request`. +// +// Doc: https://golang.org/doc/go1.13#net/http + +package jira + +import ( + "context" + "io" + "net/http" +) + +func newRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*http.Request, error) { + r, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + return r.WithContext(ctx), nil +} diff --git a/resolution.go b/resolution.go index 489f609f..e23d5650 100644 --- a/resolution.go +++ b/resolution.go @@ -1,5 +1,7 @@ package jira +import "context" + // ResolutionService handles resolutions for the Jira instance / API. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Resolution @@ -16,12 +18,12 @@ type Resolution struct { Name string `json:"name" structs:"name"` } -// GetList gets all resolutions from Jira +// GetListWithContext gets all resolutions from Jira // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-resolution-get -func (s *ResolutionService) GetList() ([]Resolution, *Response, error) { +func (s *ResolutionService) GetListWithContext(ctx context.Context) ([]Resolution, *Response, error) { apiEndpoint := "rest/api/2/resolution" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -33,3 +35,8 @@ func (s *ResolutionService) GetList() ([]Resolution, *Response, error) { } return resolutionList, resp, nil } + +// GetList wraps GetListWithContext using the background context. +func (s *ResolutionService) GetList() ([]Resolution, *Response, error) { + return s.GetListWithContext(context.Background()) +} diff --git a/role.go b/role.go index 7a3fa317..66d223ff 100644 --- a/role.go +++ b/role.go @@ -1,6 +1,7 @@ package jira import ( + "context" "fmt" ) @@ -35,12 +36,12 @@ type ActorUser struct { AccountID string `json:"accountId" structs:"accountId"` } -// GetList returns a list of all available project roles +// GetListWithContext returns a list of all available project roles // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-get -func (s *RoleService) GetList() (*[]Role, *Response, error) { +func (s *RoleService) GetListWithContext(ctx context.Context) (*[]Role, *Response, error) { apiEndpoint := "rest/api/3/role" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -53,12 +54,17 @@ func (s *RoleService) GetList() (*[]Role, *Response, error) { return roles, resp, err } -// Get retreives a single Role from Jira +// GetList wraps GetListWithContext using the background context. +func (s *RoleService) GetList() (*[]Role, *Response, error) { + return s.GetListWithContext(context.Background()) +} + +// GetWithContext retreives a single Role from Jira // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/#api-api-3-role-id-get -func (s *RoleService) Get(roleID int) (*Role, *Response, error) { +func (s *RoleService) GetWithContext(ctx context.Context, roleID int) (*Role, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/3/role/%d", roleID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -69,8 +75,13 @@ func (s *RoleService) Get(roleID int) (*Role, *Response, error) { return nil, resp, jerr } if role.Self == "" { - return nil, resp, fmt.Errorf("No role with ID %d found", roleID) + return nil, resp, fmt.Errorf("no role with ID %d found", roleID) } return role, resp, err } + +// Get wraps GetWithContext using the background context. +func (s *RoleService) Get(roleID int) (*Role, *Response, error) { + return s.GetWithContext(context.Background(), roleID) +} diff --git a/role_test.go b/role_test.go index 77197903..6e801fec 100644 --- a/role_test.go +++ b/role_test.go @@ -20,7 +20,7 @@ func TestRoleService_GetList_NoList(t *testing.T) { testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") testRequestURL(t, r, testAPIEndpoint) - fmt.Fprintf(w, string(raw)) + fmt.Fprint(w, string(raw)) }) roles, _, err := testClient.Role.GetList() @@ -44,19 +44,20 @@ func TestRoleService_GetList(t *testing.T) { testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") testRequestURL(t, r, testAPIEndpoint) - fmt.Fprintf(w, string(raw)) + fmt.Fprint(w, string(raw)) }) roles, _, err := testClient.Role.GetList() + if err != nil { + t.Errorf("Error given: %v", err) + } if roles == nil { t.Error("Expected role list. Role list is nil") + return } if len(*roles) != 2 { t.Errorf("Expected %d roles but got %d", 2, len(*roles)) } - if err != nil { - t.Errorf("Error given: %v", err) - } } func TestRoleService_Get_NoRole(t *testing.T) { @@ -70,7 +71,7 @@ func TestRoleService_Get_NoRole(t *testing.T) { testMux.HandleFunc(testAPIEdpoint, func(writer http.ResponseWriter, request *http.Request) { testMethod(t, request, "GET") testRequestURL(t, request, testAPIEdpoint) - fmt.Fprintf(writer, string(raw)) + fmt.Fprint(writer, string(raw)) }) role, _, err := testClient.Role.Get(99999) @@ -93,7 +94,7 @@ func TestRoleService_Get(t *testing.T) { testMux.HandleFunc(testAPIEdpoint, func(writer http.ResponseWriter, request *http.Request) { testMethod(t, request, "GET") testRequestURL(t, request, testAPIEdpoint) - fmt.Fprintf(writer, string(raw)) + fmt.Fprint(writer, string(raw)) }) role, _, err := testClient.Role.Get(10002) diff --git a/sprint.go b/sprint.go index 49c68968..999b59c8 100644 --- a/sprint.go +++ b/sprint.go @@ -1,6 +1,7 @@ package jira import ( + "context" "fmt" "github.com/google/go-querystring/query" @@ -22,17 +23,17 @@ type IssuesInSprintResult struct { Issues []Issue `json:"issues"` } -// MoveIssuesToSprint moves issues to a sprint, for a given sprint Id. +// MoveIssuesToSprintWithContext moves issues to a sprint, for a given sprint Id. // Issues can only be moved to open or active sprints. // The maximum number of issues that can be moved in one operation is 50. // // Jira API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint -func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) { +func (s *SprintService) MoveIssuesToSprintWithContext(ctx context.Context, sprintID int, issueIDs []string) (*Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID) payload := IssuesWrapper{Issues: issueIDs} - req, err := s.client.NewRequest("POST", apiEndpoint, payload) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, payload) if err != nil { return nil, err @@ -45,15 +46,20 @@ func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Re return resp, err } -// GetIssuesForSprint returns all issues in a sprint, for a given sprint Id. +// MoveIssuesToSprint wraps MoveIssuesToSprintWithContext using the background context. +func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) { + return s.MoveIssuesToSprintWithContext(context.Background(), sprintID, issueIDs) +} + +// GetIssuesForSprintWithContext returns all issues in a sprint, for a given sprint Id. // This only includes issues that the user has permission to view. // By default, the returned issues are ordered by rank. // -// Jira API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint -func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) { +// Jira API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint +func (s *SprintService) GetIssuesForSprintWithContext(ctx context.Context, sprintID int) ([]Issue, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -68,7 +74,12 @@ func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, er return result.Issues, resp, err } -// GetIssue returns a full representation of the issue for the given issue key. +// GetIssuesForSprint wraps GetIssuesForSprintWithContext using the background context. +func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) { + return s.GetIssuesForSprintWithContext(context.Background(), sprintID) +} + +// GetIssueWithContext returns a full representation of the issue for the given issue key. // Jira will attempt to identify the issue by the issueIdOrKey path parameter. // This can be an issue id, or an issue key. // If the issue cannot be found via an exact match, Jira will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. @@ -78,10 +89,10 @@ func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, er // Jira API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue // // TODO: create agile service for holding all agile apis' implementation -func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { +func (s *SprintService) GetIssueWithContext(ctx context.Context, issueID string, options *GetQueryOptions) (*Issue, *Response, error) { apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", issueID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -105,3 +116,8 @@ func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Iss return issue, resp, nil } + +// GetIssue wraps GetIssueWithContext using the background context. +func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { + return s.GetIssueWithContext(context.Background(), issueID, options) +} diff --git a/sprint_test.go b/sprint_test.go index 6b5c5bdd..64125dbc 100644 --- a/sprint_test.go +++ b/sprint_test.go @@ -80,8 +80,12 @@ func TestSprintService_GetIssue(t *testing.T) { }) issue, _, err := testClient.Sprint.GetIssue("10002", nil) + if err != nil { + t.Errorf("Error given: %s", err) + } if issue == nil { t.Errorf("Expected issue. Issue is nil %v", err) + return } if !reflect.DeepEqual(issue.Fields.Labels, []string{"test"}) { t.Error("Expected labels for the returned issue") @@ -89,8 +93,24 @@ func TestSprintService_GetIssue(t *testing.T) { if len(issue.Fields.Comments.Comments) != 1 { t.Errorf("Expected one comment, %v found", len(issue.Fields.Comments.Comments)) } + if len(issue.Names) != 10 { + t.Errorf("Expected 10 names, %v found", len(issue.Names)) + } + if !reflect.DeepEqual(issue.Names, map[string]string{ + "watcher": "watcher", + "attachment": "attachment", + "sub-tasks": "sub-tasks", + "description": "description", + "project": "project", + "comment": "comment", + "issuelinks": "issuelinks", + "worklog": "worklog", + "updated": "updated", + "timetracking": "timetracking", + }) { + t.Error("Expected names for the returned issue") + } if err != nil { t.Errorf("Error given: %s", err) } - } diff --git a/status.go b/status.go index 3a7e6a26..a3703929 100644 --- a/status.go +++ b/status.go @@ -1,5 +1,7 @@ package jira +import "context" + // StatusService handles staties for the Jira instance / API. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-group-Workflow-statuses @@ -19,12 +21,12 @@ type Status struct { StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"` } -// GetAllStatuses returns a list of all statuses associated with workflows. +// GetAllStatusesWithContext returns a list of all statuses associated with workflows. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/#api-rest-api-2-status-get -func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) { +func (s *StatusService) GetAllStatusesWithContext(ctx context.Context) ([]Status, *Response, error) { apiEndpoint := "rest/api/2/status" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -38,3 +40,8 @@ func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) { return statusList, resp, nil } + +// GetAllStatuses wraps GetAllStatusesWithContext using the background context. +func (s *StatusService) GetAllStatuses() ([]Status, *Response, error) { + return s.GetAllStatusesWithContext(context.Background()) +} diff --git a/statuscategory.go b/statuscategory.go index afa04b4c..bed5c566 100644 --- a/statuscategory.go +++ b/statuscategory.go @@ -1,5 +1,7 @@ package jira +import "context" + // StatusCategoryService handles status categories for the Jira instance / API. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-Statuscategory @@ -25,12 +27,12 @@ const ( StatusCategoryUndefined = "undefined" ) -// GetList gets all status categories from Jira +// GetListWithContext gets all status categories from Jira // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-statuscategory-get -func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) { +func (s *StatusCategoryService) GetListWithContext(ctx context.Context) ([]StatusCategory, *Response, error) { apiEndpoint := "rest/api/2/statuscategory" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -42,3 +44,8 @@ func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) { } return statusCategoryList, resp, nil } + +// GetList wraps GetListWithContext using the background context. +func (s *StatusCategoryService) GetList() ([]StatusCategory, *Response, error) { + return s.GetListWithContext(context.Background()) +} diff --git a/user.go b/user.go index 7444695d..237faaa0 100644 --- a/user.go +++ b/user.go @@ -1,6 +1,7 @@ package jira import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -47,16 +48,16 @@ type userSearch []userSearchParam type userSearchF func(userSearch) userSearch -// Get gets user info from Jira +// GetWithContext gets user info from Jira // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser // // /!\ Deprecation notice: https://developer.atlassian.com/cloud/jira/platform/deprecation-notice-user-privacy-api-migration-guide/ // By 29 April 2019, we will remove personal data from the API that is used to identify users, // such as username and userKey, and instead use the Atlassian account ID (accountId). -func (s *UserService) Get(username string) (*User, *Response, error) { +func (s *UserService) GetWithContext(ctx context.Context, username string) (*User, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -69,12 +70,17 @@ func (s *UserService) Get(username string) (*User, *Response, error) { return user, resp, nil } -// Get gets user info from Jira +// Get wraps GetWithContext using the background context. +func (s *UserService) Get(username string) (*User, *Response, error) { + return s.GetWithContext(context.Background(), username) +} + +// GetByAccountIDWithContext gets user info from Jira // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser -func (s *UserService) GetByAccountID(accountID string) (*User, *Response, error) { +func (s *UserService) GetByAccountIDWithContext(ctx context.Context, accountID string) (*User, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/user?accountId=%s", accountID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -87,12 +93,17 @@ func (s *UserService) GetByAccountID(accountID string) (*User, *Response, error) return user, resp, nil } -// Create creates an user in Jira. +// GetByAccountID wraps GetByAccountIDWithContext using the background context. +func (s *UserService) GetByAccountID(accountID string) (*User, *Response, error) { + return s.GetByAccountIDWithContext(context.Background(), accountID) +} + +// CreateWithContext creates an user in Jira. // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser -func (s *UserService) Create(user *User) (*User, *Response, error) { +func (s *UserService) CreateWithContext(ctx context.Context, user *User) (*User, *Response, error) { apiEndpoint := "/rest/api/2/user" - req, err := s.client.NewRequest("POST", apiEndpoint, user) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, user) if err != nil { return nil, nil, err } @@ -106,24 +117,29 @@ func (s *UserService) Create(user *User) (*User, *Response, error) { defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - e := fmt.Errorf("Could not read the returned data") + e := fmt.Errorf("could not read the returned data") return nil, resp, NewJiraError(resp, e) } err = json.Unmarshal(data, responseUser) if err != nil { - e := fmt.Errorf("Could not unmarshall the data into struct") + e := fmt.Errorf("could not unmarshall the data into struct") return nil, resp, NewJiraError(resp, e) } return responseUser, resp, nil } -// Delete deletes an user from Jira. +// Create wraps CreateWithContext using the background context. +func (s *UserService) Create(user *User) (*User, *Response, error) { + return s.CreateWithContext(context.Background(), user) +} + +// DeleteWithContext deletes an user from Jira. // Returns http.StatusNoContent on success. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-user-delete -func (s *UserService) Delete(username string) (*Response, error) { +func (s *UserService) DeleteWithContext(ctx context.Context, username string) (*Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username) - req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "DELETE", apiEndpoint, nil) if err != nil { return nil, err } @@ -135,12 +151,17 @@ func (s *UserService) Delete(username string) (*Response, error) { return resp, nil } -// GetGroups returns the groups which the user belongs to +// Delete wraps DeleteWithContext using the background context. +func (s *UserService) Delete(username string) (*Response, error) { + return s.DeleteWithContext(context.Background(), username) +} + +// GetGroupsWithContext returns the groups which the user belongs to // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUserGroups -func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error) { +func (s *UserService) GetGroupsWithContext(ctx context.Context, username string) (*[]UserGroup, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/user/groups?username=%s", username) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -153,12 +174,17 @@ func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error return userGroups, resp, nil } -// Get information about the current logged-in user +// GetGroups wraps GetGroupsWithContext using the background context. +func (s *UserService) GetGroups(username string) (*[]UserGroup, *Response, error) { + return s.GetGroupsWithContext(context.Background(), username) +} + +// GetSelfWithContext information about the current logged-in user // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-myself-get -func (s *UserService) GetSelf() (*User, *Response, error) { +func (s *UserService) GetSelfWithContext(ctx context.Context) (*User, *Response, error) { const apiEndpoint = "rest/api/2/myself" - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -170,6 +196,11 @@ func (s *UserService) GetSelf() (*User, *Response, error) { return &user, resp, nil } +// GetSelf wraps GetSelfWithContext using the background context. +func (s *UserService) GetSelf() (*User, *Response, error) { + return s.GetSelfWithContext(context.Background()) +} + // WithMaxResults sets the max results to return func WithMaxResults(maxResults int) userSearchF { return func(s userSearch) userSearch { @@ -202,11 +233,11 @@ func WithInactive(inactive bool) userSearchF { } } -// Find searches for user info from Jira: +// FindWithContext searches for user info from Jira: // It can find users by email, username or name // // Jira API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-findUsers -func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) { +func (s *UserService) FindWithContext(ctx context.Context, property string, tweaks ...userSearchF) ([]User, *Response, error) { search := []userSearchParam{ { name: "username", @@ -223,7 +254,7 @@ func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Res } apiEndpoint := fmt.Sprintf("/rest/api/2/user/search?%s", queryString[:len(queryString)-1]) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -235,3 +266,8 @@ func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Res } return users, resp, nil } + +// Find wraps FindWithContext using the background context. +func (s *UserService) Find(property string, tweaks ...userSearchF) ([]User, *Response, error) { + return s.FindWithContext(context.Background(), property, tweaks...) +} diff --git a/version.go b/version.go index 18fc33db..757c25b9 100644 --- a/version.go +++ b/version.go @@ -1,6 +1,7 @@ package jira import ( + "context" "encoding/json" "fmt" "io/ioutil" @@ -27,12 +28,12 @@ type Version struct { StartDate string `json:"startDate,omitempty" structs:"startDate,omitempty"` } -// Get gets version info from Jira +// GetWithContext gets version info from Jira // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-get -func (s *VersionService) Get(versionID int) (*Version, *Response, error) { +func (s *VersionService) GetWithContext(ctx context.Context, versionID int) (*Version, *Response, error) { apiEndpoint := fmt.Sprintf("/rest/api/2/version/%v", versionID) - req, err := s.client.NewRequest("GET", apiEndpoint, nil) + req, err := s.client.NewRequestWithContext(ctx, "GET", apiEndpoint, nil) if err != nil { return nil, nil, err } @@ -45,12 +46,17 @@ func (s *VersionService) Get(versionID int) (*Version, *Response, error) { return version, resp, nil } -// Create creates a version in Jira. +// Get wraps GetWithContext using the background context. +func (s *VersionService) Get(versionID int) (*Version, *Response, error) { + return s.GetWithContext(context.Background(), versionID) +} + +// CreateWithContext creates a version in Jira. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-post -func (s *VersionService) Create(version *Version) (*Version, *Response, error) { +func (s *VersionService) CreateWithContext(ctx context.Context, version *Version) (*Version, *Response, error) { apiEndpoint := "/rest/api/2/version" - req, err := s.client.NewRequest("POST", apiEndpoint, version) + req, err := s.client.NewRequestWithContext(ctx, "POST", apiEndpoint, version) if err != nil { return nil, nil, err } @@ -64,23 +70,28 @@ func (s *VersionService) Create(version *Version) (*Version, *Response, error) { defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) if err != nil { - e := fmt.Errorf("Could not read the returned data") + e := fmt.Errorf("could not read the returned data") return nil, resp, NewJiraError(resp, e) } err = json.Unmarshal(data, responseVersion) if err != nil { - e := fmt.Errorf("Could not unmarshall the data into struct") + e := fmt.Errorf("could not unmarshall the data into struct") return nil, resp, NewJiraError(resp, e) } return responseVersion, resp, nil } -// Update updates a version from a JSON representation. +// Create wraps CreateWithContext using the background context. +func (s *VersionService) Create(version *Version) (*Version, *Response, error) { + return s.CreateWithContext(context.Background(), version) +} + +// UpdateWithContext updates a version from a JSON representation. // // Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/#api-api-2-version-id-put -func (s *VersionService) Update(version *Version) (*Version, *Response, error) { +func (s *VersionService) UpdateWithContext(ctx context.Context, version *Version) (*Version, *Response, error) { apiEndpoint := fmt.Sprintf("rest/api/2/version/%v", version.ID) - req, err := s.client.NewRequest("PUT", apiEndpoint, version) + req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, version) if err != nil { return nil, nil, err } @@ -95,3 +106,8 @@ func (s *VersionService) Update(version *Version) (*Version, *Response, error) { ret := *version return &ret, resp, nil } + +// Update wraps UpdateWithContext using the background context. +func (s *VersionService) Update(version *Version) (*Version, *Response, error) { + return s.UpdateWithContext(context.Background(), version) +}