1
0
mirror of https://github.com/interviewstreet/go-jira.git synced 2025-03-21 21:07:03 +02:00

feat(issues): Added support for AddWorklog and GetWorklogs

This commit is contained in:
falnyr 2019-05-05 12:22:09 +12:00 committed by Wes McNamee
parent a9350ed566
commit 1ebd7e7f0d
7 changed files with 210 additions and 120 deletions

View File

@ -3,11 +3,11 @@ language: go
sudo: false
go:
- "1.7.x"
- "1.8.x"
- "1.9.x"
- "1.10.x"
- "1.11.x"
- "1.12.x"
before_install:
- go get -t ./...

36
Gopkg.lock generated
View File

@ -1,36 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/fatih/structs"
packages = ["."]
revision = "a720dfa8df582c51dee1b36feabb906bde1588bd"
version = "v1.0"
[[projects]]
branch = "master"
name = "github.com/google/go-querystring"
packages = ["query"]
revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/trivago/tgo"
packages = [
"tcontainer",
"treflect"
]
revision = "e4d1ddd28c17dd89ed26327cf69fded22060671b"
version = "v1.0.1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "e84ca9eea6d233e0947b0d760913db2983fd4cbf6fd0d8690c737a71affb635c"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -1,46 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/fatih/structs"
version = "1.0.0"
[[constraint]]
branch = "master"
name = "github.com/google/go-querystring"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
name = "github.com/trivago/tgo"
version = "1.0.1"
[prune]
go-tests = true
unused-packages = true

View File

@ -17,15 +17,18 @@
This package is not JIRA API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of JIRA have a look at [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/).
## Compatible JIRA versions
## Requirements
This package was tested against JIRA v6.3.4 and v7.1.2.
* Go >= 1.8
* JIRA v6.3.4 & v7.1.2.
## Installation
It is go gettable
$ go get github.com/andygrunwald/go-jira
```bash
go get github.com/andygrunwald/go-jira
```
For stable versions you can use one of our tags with [gopkg.in](http://labix.org/gopkg.in). E.g.
@ -40,8 +43,10 @@ import (
(optional) to run unit / example tests:
$ cd $GOPATH/src/github.com/andygrunwald/go-jira
$ go test -v ./...
```bash
cd $GOPATH/src/github.com/andygrunwald/go-jira
go test -v ./...
```
## API
@ -239,9 +244,9 @@ If you are new to pull requests, checkout [Collaborating on projects using issue
### Dependency management
`go-jira` uses `dep` for dependency management. After cloning the repo, it's easy to make sure you have the correct dependencies by running `dep ensure`.
`go-jira` uses `go modules` for dependency management. After cloning the repo, it's easy to make sure you have the correct dependencies by running `go mod tidy`.
For adding new dependencies, updating dependencies, and other operations, the [Daily Dep](https://golang.github.io/dep/docs/daily-dep.html) is a good place to start.
For adding new dependencies, updating dependencies, and other operations, the [Daily workflow](https://github.com/golang/go/wiki/Modules#daily-workflow) is a good place to start.
### Sandbox environment for testing

12
go.mod Normal file
View File

@ -0,0 +1,12 @@
module github.com/andygrunwald/go-jira
go 1.12
require (
github.com/fatih/structs v1.0.0
github.com/google/go-cmp v0.3.0
github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135
github.com/pkg/errors v0.8.0
github.com/trivago/tgo v1.0.1
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734
)

View File

@ -7,6 +7,7 @@ import (
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"reflect"
"strings"
@ -295,6 +296,10 @@ type Parent struct {
// Time represents the Time definition of JIRA as a time.Time of go
type Time time.Time
func (t Time) Equal(u Time) bool {
return time.Time(t).Equal(time.Time(u))
}
// Date represents the Date definition of JIRA as a time.Time of go
type Date time.Time
@ -394,17 +399,23 @@ type Worklog struct {
// WorklogRecord represents one entry of a Worklog
type WorklogRecord struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
Author *User `json:"author,omitempty" structs:"author,omitempty"`
UpdateAuthor *User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"`
Comment string `json:"comment,omitempty" structs:"comment,omitempty"`
Created *Time `json:"created,omitempty" structs:"created,omitempty"`
Updated *Time `json:"updated,omitempty" structs:"updated,omitempty"`
Started *Time `json:"started,omitempty" structs:"started,omitempty"`
TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"`
TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
IssueID string `json:"issueId,omitempty" structs:"issueId,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
Author *User `json:"author,omitempty" structs:"author,omitempty"`
UpdateAuthor *User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"`
Comment string `json:"comment,omitempty" structs:"comment,omitempty"`
Created *Time `json:"created,omitempty" structs:"created,omitempty"`
Updated *Time `json:"updated,omitempty" structs:"updated,omitempty"`
Started *Time `json:"started,omitempty" structs:"started,omitempty"`
TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"`
TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
IssueID string `json:"issueId,omitempty" structs:"issueId,omitempty"`
Properties []EntityProperty `json:"properties,omitempty"`
}
type EntityProperty struct {
Key string `json:"key"`
Value interface{} `json:"value"`
}
// TimeTracking represents the timetracking fields of a JIRA issue.
@ -527,6 +538,22 @@ type GetQueryOptions struct {
ProjectKeys string `url:"projectKeys,omitempty"`
}
// GetWorklogsQueryOptions specifies the optional parameters for the Get Worklogs method
type GetWorklogsQueryOptions struct {
StartAt int64 `url:"startAt,omitempty"`
MaxResults int32 `url:"maxResults,omitempty"`
Expand string `url:"expand,omitempty"`
}
type AddWorklogQueryOptions struct {
NotifyUsers bool `url:"notifyUsers,omitempty"`
AdjustEstimate string `url:"adjustEstimate,omitempty"`
NewEstimate string `url:"newEstimate,omitempty"`
ReduceBy string `url:"reduceBy,omitempty"`
Expand string `url:"expand,omitempty"`
OverrideEditableFlag bool `url:"overrideEditableFlag,omitempty"`
}
// CustomFields represents custom fields of JIRA
// This can heavily differ between JIRA instances
type CustomFields map[string]string
@ -626,7 +653,7 @@ func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentNam
// 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) (*Worklog, *Response, error) {
func (s *IssueService) GetWorklogs(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)
@ -634,11 +661,34 @@ func (s *IssueService) GetWorklogs(issueID string) (*Worklog, *Response, error)
return nil, nil, err
}
for _, option := range options {
err = option(req)
if err != nil {
return nil, nil, err
}
}
v := new(Worklog)
resp, err := s.client.Do(req, v)
return v, resp, err
}
// 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 {
q, err := query.Values(options)
if err != nil {
return func(*http.Request) error {
return err
}
}
return func(r *http.Request) error {
r.URL.RawQuery = q.Encode()
return nil
}
}
// Create 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.
@ -787,13 +837,20 @@ func (s *IssueService) DeleteComment(issueID, commentID string) error {
// AddWorklogRecord 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) (*WorklogRecord, *Response, error) {
func (s *IssueService) AddWorklogRecord(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)
if err != nil {
return nil, nil, err
}
for _, option := range options {
err = option(req)
if err != nil {
return nil, nil, err
}
}
responseRecord := new(WorklogRecord)
resp, err := s.client.Do(req, responseRecord)
if err != nil {

View File

@ -3,6 +3,7 @@ package jira
import (
"encoding/json"
"fmt"
"github.com/google/go-cmp/cmp"
"io"
"io/ioutil"
"net/http"
@ -1316,31 +1317,128 @@ func TestIssueService_Delete(t *testing.T) {
}
}
func getTime(original time.Time) *Time {
jiraTime := Time(original)
return &jiraTime
}
func TestIssueService_GetWorklogs(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issue/10002/worklog", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, "/rest/api/2/issue/10002/worklog")
fmt.Fprint(w, `{"startAt": 1,"maxResults": 40,"total": 1,"worklogs": [{"id": "3","self": "http://kelpie9:8081/rest/api/2/issue/10002/worklog/3","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.356+0000","updated":"2016-03-16T04:22:37.356+0000","comment":"","started":"2016-03-16T04:22:37.356+0000","timeSpent": "1h","timeSpentSeconds": 3600,"issueId":"10002"}]}`)
})
worklog, _, err := testClient.Issue.GetWorklogs("10002")
if worklog == nil {
t.Error("Expected worklog. Worklog is nil")
tt := []struct {
name string
response string
issueId string
uri string
worklog *Worklog
err error
option *AddWorklogQueryOptions
}{
{
name: "simple worklog",
response: `{"startAt": 1,"maxResults": 40,"total": 1,"worklogs": [{"id": "3","self": "http://kelpie9:8081/rest/api/2/issue/10002/worklog/3","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.356+0000","updated":"2016-03-16T04:22:37.356+0000","comment":"","started":"2016-03-16T04:22:37.356+0000","timeSpent": "1h","timeSpentSeconds": 3600,"issueId":"10002"}]}`,
issueId: "10002",
uri: "/rest/api/2/issue/%s/worklog",
worklog: &Worklog{
StartAt: 1,
MaxResults: 40,
Total: 1,
Worklogs: []WorklogRecord{
{
Self: "http://kelpie9:8081/rest/api/2/issue/10002/worklog/3",
Author: &User{
Self: "http://www.example.com/jira/rest/api/2/user?username=fred",
Name: "fred",
DisplayName: "Fred F. User",
},
UpdateAuthor: &User{
Self: "http://www.example.com/jira/rest/api/2/user?username=fred",
Name: "fred",
DisplayName: "Fred F. User",
},
Created: getTime(time.Date(2016, time.March, 16, 4, 22, 37, 356000000, time.UTC)),
Started: getTime(time.Date(2016, time.March, 16, 4, 22, 37, 356000000, time.UTC)),
Updated: getTime(time.Date(2016, time.March, 16, 4, 22, 37, 356000000, time.UTC)),
TimeSpent: "1h",
TimeSpentSeconds: 3600,
ID: "3",
IssueID: "10002",
},
},
},
},
{
name: "expanded worklog",
response: `{"startAt":1,"maxResults":40,"total":1,"worklogs":[{"id":"3","self":"http://kelpie9:8081/rest/api/2/issue/10002/worklog/3","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.356+0000","updated":"2016-03-16T04:22:37.356+0000","comment":"","started":"2016-03-16T04:22:37.356+0000","timeSpent":"1h","timeSpentSeconds":3600,"issueId":"10002","properties":[{"key":"foo","value":{"bar":"baz"}}]}]}`,
issueId: "10002",
uri: "/rest/api/2/issue/%s/worklog?expand=properties",
worklog: &Worklog{
StartAt: 1,
MaxResults: 40,
Total: 1,
Worklogs: []WorklogRecord{
{
Self: "http://kelpie9:8081/rest/api/2/issue/10002/worklog/3",
Author: &User{
Self: "http://www.example.com/jira/rest/api/2/user?username=fred",
Name: "fred",
DisplayName: "Fred F. User",
},
UpdateAuthor: &User{
Self: "http://www.example.com/jira/rest/api/2/user?username=fred",
Name: "fred",
DisplayName: "Fred F. User",
},
Created: getTime(time.Date(2016, time.March, 16, 4, 22, 37, 356000000, time.UTC)),
Started: getTime(time.Date(2016, time.March, 16, 4, 22, 37, 356000000, time.UTC)),
Updated: getTime(time.Date(2016, time.March, 16, 4, 22, 37, 356000000, time.UTC)),
TimeSpent: "1h",
TimeSpentSeconds: 3600,
ID: "3",
IssueID: "10002",
Properties: []EntityProperty{
{
Key: "foo",
Value: map[string]interface{}{
"bar": "baz",
},
},
},
},
},
},
option: &AddWorklogQueryOptions{Expand: "properties"},
},
}
if len(worklog.Worklogs) != 1 {
t.Error("Expected 1 worklog")
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
uri := fmt.Sprintf(tc.uri, tc.issueId)
testMux.HandleFunc(uri, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, uri)
_, _ = fmt.Fprint(w, tc.response)
})
if worklog.Worklogs[0].Author.Name != "fred" {
t.Error("Expected worklog author to be fred")
}
var worklog *Worklog
var err error
if err != nil {
t.Errorf("Error given: %s", err)
if tc.option != nil {
worklog, _, err = testClient.Issue.GetWorklogs(tc.issueId, WithQueryOptions(tc.option))
} else {
worklog, _, err = testClient.Issue.GetWorklogs(tc.issueId)
}
if err != nil && !cmp.Equal(err, tc.err) {
t.Errorf("unexpected error: %v", err)
}
if !cmp.Equal(worklog, tc.worklog) {
t.Errorf("unexpected worklog structure: %s", cmp.Diff(worklog, tc.worklog))
}
})
}
}