From eb07612cbfadc0a8a6938a9d6e2d8485b0ddbd1c Mon Sep 17 00:00:00 2001 From: Bidesh Thapaliya Date: Wed, 7 Sep 2016 14:24:02 +0200 Subject: [PATCH 1/7] Completes the APi for session. Adds logout and GetCurrentUser --- authentication.go | 85 ++++++++++++++++++++++++++--- authentication_test.go | 119 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 7 deletions(-) diff --git a/authentication.go b/authentication.go index e593469..59fdf88 100644 --- a/authentication.go +++ b/authentication.go @@ -1,7 +1,9 @@ package jira import ( + "encoding/json" "fmt" + "io/ioutil" "net/http" ) @@ -54,9 +56,9 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string) session := new(Session) resp, err := s.client.Do(req, session) - if resp != nil { - session.Cookies = resp.Cookies() - } + if resp != nil { + session.Cookies = resp.Cookies() + } if err != nil { return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err) @@ -78,7 +80,76 @@ func (s *AuthenticationService) Authenticated() bool { return false } -// TODO Missing API Call GET (Returns information about the currently authenticated user's session) -// See https://docs.atlassian.com/jira/REST/latest/#auth/1/session -// TODO Missing API Call DELETE (Logs the current user out of JIRA, destroying the existing session, if any.) -// See https://docs.atlassian.com/jira/REST/latest/#auth/1/session +// Logout 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 +func (s *AuthenticationService) Logout() error { + if s == nil { + return fmt.Errorf("Authenticaiton Service is not instantiated") + } + if s.client.session == nil { + return fmt.Errorf("No user is authenticated yet.") + } + + apiEndpoint := "rest/auth/1/session" + req, err := s.client.NewRequest("DELETE", apiEndpoint, nil) + if err != nil { + return fmt.Errorf("Creating the request to log the user out failed : %s", err) + } + //var dump interface{} + resp, err := s.client.Do(req, nil) + if err != nil { + 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) + } + + // if logout successfull, delete session + s.client.session = nil + + return nil + +} + +// GetCurrentUser 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) { + if s == nil { + return nil, fmt.Errorf("AUthenticaiton Service is not instantiated") + } + if s.client.session == nil { + return nil, fmt.Errorf("No user is authenticated yet") + } + + apiEndpoint := "rest/auth/1/session" + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + 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) + } + if resp.StatusCode != 200 { + 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) + } + + err = json.Unmarshal(data, &ret) + + if err != nil { + return nil, fmt.Errorf("Could not unmarshall recieved user info : %s", err) + } + + return ret, nil + +} diff --git a/authentication_test.go b/authentication_test.go index d6741a3..2f0426b 100644 --- a/authentication_test.go +++ b/authentication_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "net/http" + "reflect" "testing" ) @@ -84,3 +85,121 @@ func TestAuthenticationService_Authenticated(t *testing.T) { t.Error("Expected false, but result was true") } } + +func TestAuthenticationService_GetUserInfo_FailWithoutLogin(t *testing.T) { + // no setup() required here + testClient = new(Client) + + _, err := testClient.Authentication.GetCurrentUser() + if err == nil { + t.Errorf("Expected error, but got %s", err) + } +} + +func TestAuthenticationService_GetUserInfo_Success(t *testing.T) { + setup() + defer teardown() + + testUserInfo := new(Session) + testUserInfo.Name = "foo" + testUserInfo.Self = "https://tasks.trivago.com/rest/api/latest/user?username=foo" + testUserInfo.LoginInfo.FailedLoginCount = 12 + testUserInfo.LoginInfo.LastFailedLoginTime = "2016-09-06T16:41:23.949+0200" + testUserInfo.LoginInfo.LoginCount = 357 + testUserInfo.LoginInfo.PreviousLoginTime = "2016-09-07T11:36:23.476+0200" + + testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + testMethod(t, r, "POST") + testRequestURL(t, r, "/rest/auth/1/session") + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Error in read body: %s", err) + } + if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + t.Error("No username found") + } + if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + t.Error("No password found") + } + + fmt.Fprint(w, `{"session":{"name":"JSESSIONID","value":"12345678901234567890"},"loginInfo":{"failedLoginCount":10,"loginCount":127,"lastFailedLoginTime":"2016-03-16T04:22:35.386+0000","previousLoginTime":"2016-03-16T04:22:35.386+0000"}}`) + } + + if r.Method == "GET" { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/auth/1/session") + fmt.Fprint(w, `{"self":"https://tasks.trivago.com/rest/api/latest/user?username=foo","name":"foo","loginInfo":{"failedLoginCount":12,"loginCount":357,"lastFailedLoginTime":"2016-09-06T16:41:23.949+0200","previousLoginTime":"2016-09-07T11:36:23.476+0200"}}`) + } + + }) + + testClient.Authentication.AcquireSessionCookie("foo", "bar") + + userinfo, err := testClient.Authentication.GetCurrentUser() + if err != nil { + t.Errorf("Nil error expect, recieved %s", err) + } + equal := reflect.DeepEqual(*testUserInfo, *userinfo) + + if !equal { + t.Error("The user information doesn't match") + } +} + +func TestAuthenticationService_Logout_Success(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + testMethod(t, r, "POST") + testRequestURL(t, r, "/rest/auth/1/session") + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Error in read body: %s", err) + } + if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + t.Error("No username found") + } + if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + t.Error("No password found") + } + + fmt.Fprint(w, `{"session":{"name":"JSESSIONID","value":"12345678901234567890"},"loginInfo":{"failedLoginCount":10,"loginCount":127,"lastFailedLoginTime":"2016-03-16T04:22:35.386+0000","previousLoginTime":"2016-03-16T04:22:35.386+0000"}}`) + } + + if r.Method == "DELETE" { + // return 204 + w.WriteHeader(http.StatusNoContent) + } + + }) + + testClient.Authentication.AcquireSessionCookie("foo", "bar") + + err := testClient.Authentication.Logout() + if err != nil { + t.Errorf("Expected nil error, got %s", err) + } + +} + +func TestAuthenticationService_Logout_FailWithoutLogin(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "DELETE" { + // 401 + w.WriteHeader(http.StatusUnauthorized) + } + + }) + + err := testClient.Authentication.Logout() + if err == nil { + t.Error("Expected not nil, got nil") + } + +} From fe6b1291723604b9b3aa4c6c8fb997811bb061e9 Mon Sep 17 00:00:00 2001 From: Bidesh Thapaliya Date: Tue, 20 Sep 2016 14:27:54 +0200 Subject: [PATCH 2/7] Adds metaissue support. --- metaissue.go | 121 +++++++++++++++ metaissue_test.go | 379 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 500 insertions(+) create mode 100644 metaissue.go create mode 100644 metaissue_test.go diff --git a/metaissue.go b/metaissue.go new file mode 100644 index 0000000..620ebac --- /dev/null +++ b/metaissue.go @@ -0,0 +1,121 @@ +package jira + +import ( + "fmt" + "strings" +) + +// CreateMeta contains information about fields and their attributed to create a ticket. +type CreateMetaInfo struct { + Expand string `json:"expand,omitempty"` + Projects []*MetaProject `json:"projects,omitempty"` +} + +// MetaProject is the meta information about a project returned from createmeta api +type MetaProject struct { + Expand string `json:"expand,omitempty"` + Self string `json:"self, omitempty"` + Id string `json:"id,omitempty"` + Key string `json:"key,omitempty"` + Name string `json:"name,omitempty"` + // omitted avatarUrls + IssueTypes []*MetaIssueTypes `json:"issuetypes,omitempty"` +} + +// metaIssueTypes represents the different issue types a project has. +// +// Note: Fields is interface because this is an object which can +// have arbitraty keys related to customfields. It is not possible to +// expect these for a general way. This will be returning a map. +// Further processing must be done depending on what is required. +type MetaIssueTypes struct { + Self string `json:"expand,omitempty"` + Id string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + IconUrl string `json:"iconurl,omitempty"` + Name string `json:"name,omitempty"` + Subtasks bool `json:"subtask,omitempty"` + Expand string `json:"expand,omitempty"` + Fields map[string]interface{} `json:"fields,omitempty"` +} + +// GetCreateMeta makes the api call to get the meta information required to create a ticket +func (s *IssueService) GetCreateMeta(projectkey string) (*CreateMetaInfo, *Response, error) { + + apiEndpoint := fmt.Sprintf("/rest/api/2/issue/createmeta?projectKeys=%s&expand=projects.issuetypes.fields", projectkey) + + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + fmt.Println(req.URL) + meta := new(CreateMetaInfo) + resp, err := s.client.Do(req, meta) + + if err != nil { + return nil, resp, err + } + + return meta, resp, nil +} + +// GetProjectWithName returns a project with "name" from the meta information recieved. If not found, this returns nil. +// The comparision 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) { + return m + } + } + return nil +} + +// GetIssueWithName returns an IssueType with name from a given MetaProject. If not found, this returns nil. +// The comparision of the name is case insensitive +func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueTypes { + for _, m := range p.IssueTypes { + if strings.ToLower(m.Name) == strings.ToLower(name) { + return m + } + } + return nil +} + +// GetMandatoryFields returns a map of all the required fields from the MetaIssueTypes. +// if a frield returned by the api was: +// "customfield_10806": { +// "required": true, +// "schema": { +// "type": "any", +// "custom": "com.pyxis.greenhopper.jira:gh-epic-link", +// "customId": 10806 +// }, +// "name": "Epic Link", +// "hasDefaultValue": false, +// "operations": [ +// "set" +// ] +// } +// the returned map would have "Epic Link" as the key and "customfield_10806" as value. +// This choice has been made so that the it is easier to generate the create api request later. +func (t *MetaIssueTypes) GetMandatoryFields() map[string]string { + ret := make(map[string]string) + for key, obj := range t.Fields { + details := obj.(map[string]interface{}) + if details["required"] == true { + ret[details["name"].(string)] = key + } + } + return nil +} + +// GetAllFields returns a map of all the fields for an IssueType. This includes all required and not required. +// The key of the returned map is what you see in the form and the value is how it is representated in the jira schema. +func (t *MetaIssueTypes) GetAllFields() map[string]string { + ret := make(map[string]string) + for key, obj := range t.Fields { + details := obj.(map[string]interface{}) + ret[details["name"].(string)] = key + } + return ret +} diff --git a/metaissue_test.go b/metaissue_test.go new file mode 100644 index 0000000..9644145 --- /dev/null +++ b/metaissue_test.go @@ -0,0 +1,379 @@ +package jira + +import ( + "fmt" + "net/http" + "testing" +) + +func TestIssueService_GetCreateMeta_Success(t *testing.T) { + setup() + defer teardown() + + testApiEndpoint := "/rest/api/2/issue/createmeta" + + testMux.HandleFunc(testApiEndpoint, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, testApiEndpoint) + + fmt.Fprint(w, `{ + "expand": "projects", + "projects": [{ + "expand": "issuetypes", + "self": "https://tasks.trivago.com/rest/api/2/project/11300", + "id": "11300", + "key": "SOP", + "name": "DSE - Software Operations", + "avatarUrls": { + "48x48": "https://tasks.trivago.com/secure/projectavatar?pid=11300&avatarId=14405", + "24x24": "https://tasks.trivago.com/secure/projectavatar?size=small&pid=11300&avatarId=14405", + "16x16": "https://tasks.trivago.com/secure/projectavatar?size=xsmall&pid=11300&avatarId=14405", + "32x32": "https://tasks.trivago.com/secure/projectavatar?size=medium&pid=11300&avatarId=14405" + }, + "issuetypes": [{ + "self": "https://tasks.trivago.com/rest/api/2/issuetype/6", + "id": "6", + "description": "An issue which ideally should be able to be completed in one step", + "iconUrl": "https://tasks.trivago.com/secure/viewavatar?size=xsmall&avatarId=14006&avatarType=issuetype", + "name": "Request", + "subtask": false, + "expand": "fields", + "fields": { + "summary": { + "required": true, + "schema": { + "type": "string", + "system": "summary" + }, + "name": "Summary", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "issuetype": { + "required": true, + "schema": { + "type": "issuetype", + "system": "issuetype" + }, + "name": "Issue Type", + "hasDefaultValue": false, + "operations": [ + + ], + "allowedValues": [{ + "self": "https://tasks.trivago.com/rest/api/2/issuetype/6", + "id": "6", + "description": "An issue which ideally should be able to be completed in one step", + "iconUrl": "https://tasks.trivago.com/secure/viewavatar?size=xsmall&avatarId=14006&avatarType=issuetype", + "name": "Request", + "subtask": false, + "avatarId": 14006 + }] + }, + "components": { + "required": true, + "schema": { + "type": "array", + "items": "component", + "system": "components" + }, + "name": "Component/s", + "hasDefaultValue": false, + "operations": [ + "add", + "set", + "remove" + ], + "allowedValues": [{ + "self": "https://tasks.trivago.com/rest/api/2/component/14144", + "id": "14144", + "name": "Build automation", + "description": "Jenkins, webhooks, etc." + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14149", + "id": "14149", + "name": "Caches and noSQL", + "description": "Cassandra, Memcached, Redis, Twemproxy, Xcache" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14152", + "id": "14152", + "name": "Cloud services", + "description": "AWS and similiar services" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14147", + "id": "14147", + "name": "Code quality tools", + "description": "Code sniffer, Arqtig, Sonar" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14156", + "id": "14156", + "name": "Configuration management and provisioning", + "description": "Apache/PHP modules, Consul, Salt" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/13606", + "id": "13606", + "name": "Cronjobs", + "description": "Cronjobs in general" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14150", + "id": "14150", + "name": "Data pipelines and queues", + "description": "Gollum, Kafka, RabbitMq" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14159", + "id": "14159", + "name": "Database", + "description": "MySQL related problems" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14314", + "id": "14314", + "name": "Documentation" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14151", + "id": "14151", + "name": "Git", + "description": "Bitbucket, GitHub, GitLab, Git in general" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14155", + "id": "14155", + "name": "HTTP services", + "description": "CDN, HaProxy, HTTP, Varnish" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14154", + "id": "14154", + "name": "Job and service scheduling", + "description": "Chronos, Docker, Marathon, Mesos" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14158", + "id": "14158", + "name": "Legacy", + "description": "Everything related to legacy" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14157", + "id": "14157", + "name": "Monitoring", + "description": "Collectd, Nagios, Monitoring in general" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14148", + "id": "14148", + "name": "Other services" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/13602", + "id": "13602", + "name": "Package management", + "description": "Composer, Medusa, Satis" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14145", + "id": "14145", + "name": "Release", + "description": "Directory config, release queries, rewrite rules" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14146", + "id": "14146", + "name": "Staging systems and VMs", + "description": "Stage, QA machines, KVMs,Vagrant" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14153", + "id": "14153", + "name": "Techblog" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14143", + "id": "14143", + "name": "Test automation", + "description": "Testing infrastructure in general" + }, { + "self": "https://tasks.trivago.com/rest/api/2/component/14221", + "id": "14221", + "name": "Zup" + }] + }, + "attachment": { + "required": false, + "schema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "name": "Attachment", + "hasDefaultValue": false, + "operations": [ + + ] + }, + "duedate": { + "required": false, + "schema": { + "type": "date", + "system": "duedate" + }, + "name": "Due Date", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "description": { + "required": false, + "schema": { + "type": "string", + "system": "description" + }, + "name": "Description", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "customfield_10806": { + "required": false, + "schema": { + "type": "any", + "custom": "com.pyxis.greenhopper.jira:gh-epic-link", + "customId": 10806 + }, + "name": "Epic Link", + "hasDefaultValue": false, + "operations": [ + "set" + ] + }, + "project": { + "required": true, + "schema": { + "type": "project", + "system": "project" + }, + "name": "Project", + "hasDefaultValue": false, + "operations": [ + "set" + ], + "allowedValues": [{ + "self": "https://tasks.trivago.com/rest/api/2/project/11300", + "id": "11300", + "key": "SOP", + "name": "DSE - Software Operations", + "avatarUrls": { + "48x48": "https://tasks.trivago.com/secure/projectavatar?pid=11300&avatarId=14405", + "24x24": "https://tasks.trivago.com/secure/projectavatar?size=small&pid=11300&avatarId=14405", + "16x16": "https://tasks.trivago.com/secure/projectavatar?size=xsmall&pid=11300&avatarId=14405", + "32x32": "https://tasks.trivago.com/secure/projectavatar?size=medium&pid=11300&avatarId=14405" + }, + "projectCategory": { + "self": "https://tasks.trivago.com/rest/api/2/projectCategory/10100", + "id": "10100", + "description": "", + "name": "Product & Development" + } + }] + }, + "assignee": { + "required": true, + "schema": { + "type": "user", + "system": "assignee" + }, + "name": "Assignee", + "autoCompleteUrl": "https://tasks.trivago.com/rest/api/latest/user/assignable/search?issueKey=null&username=", + "hasDefaultValue": true, + "operations": [ + "set" + ] + }, + "priority": { + "required": false, + "schema": { + "type": "priority", + "system": "priority" + }, + "name": "Priority", + "hasDefaultValue": true, + "operations": [ + "set" + ], + "allowedValues": [{ + "self": "https://tasks.trivago.com/rest/api/2/priority/1", + "iconUrl": "https://tasks.trivago.com/images/icons/priorities/blocker.svg", + "name": "Immediate", + "id": "1" + }, { + "self": "https://tasks.trivago.com/rest/api/2/priority/2", + "iconUrl": "https://tasks.trivago.com/images/icons/priorities/critical.svg", + "name": "Urgent", + "id": "2" + }, { + "self": "https://tasks.trivago.com/rest/api/2/priority/3", + "iconUrl": "https://tasks.trivago.com/images/icons/priorities/major.svg", + "name": "High", + "id": "3" + }, { + "self": "https://tasks.trivago.com/rest/api/2/priority/6", + "iconUrl": "https://tasks.trivago.com/images/icons/priorities/moderate.svg", + "name": "Moderate", + "id": "6" + }, { + "self": "https://tasks.trivago.com/rest/api/2/priority/4", + "iconUrl": "https://tasks.trivago.com/images/icons/priorities/minor.svg", + "name": "Normal", + "id": "4" + }, { + "self": "https://tasks.trivago.com/rest/api/2/priority/5", + "iconUrl": "https://tasks.trivago.com/images/icons/priorities/trivial.svg", + "name": "Low", + "id": "5" + }] + }, + "labels": { + "required": false, + "schema": { + "type": "array", + "items": "string", + "system": "labels" + }, + "name": "Labels", + "autoCompleteUrl": "https://tasks.trivago.com/rest/api/1.0/labels/suggest?query=", + "hasDefaultValue": false, + "operations": [ + "add", + "set", + "remove" + ] + } + } + }] + }] + }`) + }) + + issue, _, err := testClient.Issue.GetCreateMeta("SOP") + if err != nil { + t.Errorf("Expected nil error but got %s", err) + } + + if len(issue.Projects) != 1 { + t.Errorf("Expected 1 project, got %d", len(issue.Projects)) + } + for _, project := range issue.Projects { + if len(project.IssueTypes) != 1 { + t.Errorf("Expected 1 issueTypes, got %d", len(project.IssueTypes)) + } + for _, issueTypes := range project.IssueTypes { + requiredFields := 0 + fields := issueTypes.Fields + for _, value := range fields { + for key, value := range value.(map[string]interface{}) { + if key == "required" && value == true { + requiredFields = requiredFields + 1 + } + } + + } + if requiredFields != 5 { + t.Errorf("Expected 5 required fields from Create Meta information, got %d", requiredFields) + } + } + } + +} From 5ce765977fbd459859ca0cd7af4931ff6c628d57 Mon Sep 17 00:00:00 2001 From: Bidesh Thapaliya Date: Fri, 23 Sep 2016 16:19:07 +0200 Subject: [PATCH 3/7] Adds unknown map for arbitrary fields in IssueFields. Adds Custom Marshall,Unmarshall. Adds structs tag where necessary --- board.go | 38 ++--- issue.go | 369 +++++++++++++++++++++++++++------------------- issue_test.go | 127 ++++++++++++++++ metaissue.go | 50 ++++--- metaissue_test.go | 65 ++++++++ project.go | 98 ++++++------ 6 files changed, 508 insertions(+), 239 deletions(-) diff --git a/board.go b/board.go index 3072edf..a79ed93 100644 --- a/board.go +++ b/board.go @@ -14,20 +14,20 @@ type BoardService struct { // BoardsList reflects a list of agile boards type BoardsList struct { - MaxResults int `json:"maxResults"` - StartAt int `json:"startAt"` - Total int `json:"total"` - IsLast bool `json:"isLast"` - Values []Board `json:"values"` + MaxResults int `json:"maxResults" structs:"maxResults"` + StartAt int `json:"startAt" structs:"startAt"` + Total int `json:"total" structs:"total"` + IsLast bool `json:"isLast" structs:"isLast"` + Values []Board `json:"values" structs:"values"` } // Board represents a JIRA agile board type Board struct { - ID int `json:"id,omitempty"` - Self string `json:"self,omitempty"` - Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` - FilterID int `json:"filterId,omitempty"` + ID int `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitemtpy"` + Type string `json:"type,omitempty" structs:"type,omitempty"` + FilterID int `json:"filterId,omitempty" structs:"filterId,omitempty"` } // BoardListOptions specifies the optional parameters to the BoardService.GetList @@ -46,19 +46,19 @@ type BoardListOptions struct { // Wrapper struct for search result type sprintsResult struct { - Sprints []Sprint `json:"values"` + Sprints []Sprint `json:"values" structs:"values"` } // Sprint represents a sprint on JIRA agile board type Sprint struct { - ID int `json:"id"` - Name string `json:"name"` - CompleteDate *time.Time `json:"completeDate"` - EndDate *time.Time `json:"endDate"` - StartDate *time.Time `json:"startDate"` - OriginBoardID int `json:"originBoardId"` - Self string `json:"self"` - State string `json:"state"` + ID int `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + CompleteDate *time.Time `json:"completeDate" structs:"completeDate"` + EndDate *time.Time `json:"endDate" structs:"endDate"` + StartDate *time.Time `json:"startDate" structs:"startDate"` + OriginBoardID int `json:"originBoardId" structs:"originBoardId"` + Self string `json:"self" structs:"self"` + State string `json:"state" structs:"state"` } // GetAllBoards will returns all boards. This only includes boards that the user has permission to view. diff --git a/issue.go b/issue.go index b9c9610..b862aa4 100644 --- a/issue.go +++ b/issue.go @@ -2,10 +2,14 @@ package jira import ( "bytes" + "encoding/json" "fmt" + "github.com/fatih/structs" + "github.com/trivago/tgo/tcontainer" "io" "mime/multipart" "net/url" + "reflect" "strings" "time" ) @@ -24,35 +28,35 @@ type IssueService struct { // Issue represents a JIRA issue. type Issue struct { - Expand string `json:"expand,omitempty"` - ID string `json:"id,omitempty"` - Self string `json:"self,omitempty"` - Key string `json:"key,omitempty"` - Fields *IssueFields `json:"fields,omitempty"` + Expand string `json:"expand,omitempty" structs:"expand,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + Fields *IssueFields `json:"fields,omitempty" structs:"fields,omitempty"` } // Attachment represents a JIRA attachment type Attachment struct { - Self string `json:"self,omitempty"` - ID string `json:"id,omitempty"` - Filename string `json:"filename,omitempty"` - Author *User `json:"author,omitempty"` - Created string `json:"created,omitempty"` - Size int `json:"size,omitempty"` - MimeType string `json:"mimeType,omitempty"` - Content string `json:"content,omitempty"` - Thumbnail string `json:"thumbnail,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Filename string `json:"filename,omitempty" structs:"filename,omitempty"` + Author *User `json:"author,omitempty" structs:"author,omitempty"` + Created string `json:"created,omitempty" structs:"created,omitempty"` + Size int `json:"size,omitempty" structs:"size,omitempty"` + MimeType string `json:"mimeType,omitempty" structs:"mimeType,omitempty"` + Content string `json:"content,omitempty" structs:"content,omitempty"` + Thumbnail string `json:"thumbnail,omitempty" structs:"thumbnail,omitempty"` } // Epic represents the epic to which an issue is associated // Not that this struct does not process the returned "color" value type Epic struct { - ID int `json:"id"` - Key string `json:"key"` - Self string `json:"self"` - Name string `json:"name"` - Summary string `json:"summary"` - Done bool `json:"done"` + ID int `json:"id" structs:"id"` + Key string `json:"key" structs:"key"` + Self string `json:"self" structs:"self"` + Name string `json:"name" structs:"name"` + Summary string `json:"summary" structs:"summary"` + Done bool `json:"done" structs:"done"` } // IssueFields represents single fields of a JIRA issue. @@ -70,124 +74,185 @@ type IssueFields struct { // * "aggregatetimeestimate": null, // * "environment": null, // * "duedate": null, - Type IssueType `json:"issuetype"` - Project Project `json:"project,omitempty"` - Resolution *Resolution `json:"resolution,omitempty"` - Priority *Priority `json:"priority,omitempty"` - Resolutiondate string `json:"resolutiondate,omitempty"` - Created string `json:"created,omitempty"` - Watches *Watches `json:"watches,omitempty"` - Assignee *User `json:"assignee,omitempty"` - Updated string `json:"updated,omitempty"` - Description string `json:"description,omitempty"` - Summary string `json:"summary"` - Creator *User `json:"Creator,omitempty"` - Reporter *User `json:"reporter,omitempty"` - Components []*Component `json:"components,omitempty"` - Status *Status `json:"status,omitempty"` - Progress *Progress `json:"progress,omitempty"` - AggregateProgress *Progress `json:"aggregateprogress,omitempty"` - Worklog *Worklog `json:"worklog,omitempty"` - IssueLinks []*IssueLink `json:"issuelinks,omitempty"` - Comments *Comments `json:"comment,omitempty"` - FixVersions []*FixVersion `json:"fixVersions,omitempty"` - Labels []string `json:"labels,omitempty"` - Subtasks []*Subtasks `json:"subtasks,omitempty"` - Attachments []*Attachment `json:"attachment,omitempty"` - Epic *Epic `json:"epic,omitempty"` + Type IssueType `json:"issuetype" structs:"issuetype"` + Project Project `json:"project,omitempty" structs:"project,omitempty"` + Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"` + Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"` + Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"` + Created string `json:"created,omitempty" structs:"created,omitempty"` + Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"` + Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"` + Updated string `json:"updated,omitempty" structs:"updated,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` + Summary string `json:"summary" structs:"summary"` + Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"` + Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"` + Components []*Component `json:"components,omitempty" structs:"components,omitempty"` + Status *Status `json:"status,omitempty" structs:"status,omitempty"` + Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"` + AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"` + Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"` + IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"` + Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"` + FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"` + Labels []string `json:"labels,omitempty" structs:"labels,omitempty"` + Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"` + Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"` + Epic *Epic `json:"epic,omitempty" structs:"epic,omitempty"` + Unknowns tcontainer.MarshalMap +} + +func (i *IssueFields) MarshalJSON() ([]byte, error) { + m := structs.Map(i) + unknowns, okay := m["Unknowns"] + if okay { + // if unknowns present, shift all key value from unkown to a level up + for key, value := range unknowns.(tcontainer.MarshalMap) { + m[key] = value + } + delete(m, "Unknowns") + } + return json.Marshal(m) +} + +func (i *IssueFields) UnmarshalJSON(data []byte) error { + + // Do the normal unmarshalling first + // Details for this way: http://choly.ca/post/go-json-marshalling/ + type Alias IssueFields + aux := &struct { + *Alias + }{ + Alias: (*Alias)(i), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + totalMap := tcontainer.NewMarshalMap() + err := json.Unmarshal(data, &totalMap) + if err != nil { + return err + } + + t := reflect.TypeOf(*i) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + tagDetail := field.Tag.Get("json") + if tagDetail == "" { + // ignore if there are no tags + continue + } + options := strings.Split(tagDetail, ",") + + if len(options) == 0 { + return fmt.Errorf("No tags options found for %s", field.Name) + } + // the first one is the json tag + key := options[0] + if _, okay := totalMap.Value(key); okay { + delete(totalMap, key) + } + + } + i = (*IssueFields)(aux.Alias) + // all the tags found in the struct were removed. Whatever is left are unknowns to struct + i.Unknowns = totalMap + return nil + } // IssueType represents a type of a JIRA issue. // Typical types are "Request", "Bug", "Story", ... type IssueType struct { - Self string `json:"self,omitempty"` - ID string `json:"id,omitempty"` - Description string `json:"description,omitempty"` - IconURL string `json:"iconUrl,omitempty"` - Name string `json:"name,omitempty"` - Subtask bool `json:"subtask,omitempty"` - AvatarID int `json:"avatarId,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" struct:"id,omitempty"` + Description string `json:"description,omitempty" struct:"description,omitempty"` + IconURL string `json:"iconUrl,omitempty" struct:"iconUrl,omitempty"` + Name string `json:"name,omitempty" struct:"name,omitempty"` + Subtask bool `json:"subtask,omitempty" struct:"subtask,omitempty"` + AvatarID int `json:"avatarId,omitempty" struct:"avatarId,omitempty"` } // Resolution represents a resolution of a JIRA issue. // Typical types are "Fixed", "Suspended", "Won't Fix", ... type Resolution struct { - Self string `json:"self"` - ID string `json:"id"` - Description string `json:"description"` - Name string `json:"name"` + Self string `json:"self" structs:"self"` + ID string `json:"id" structs:"id"` + Description string `json:"description" structs:"description"` + Name string `json:"name" structs:"name"` } // Priority represents a priority of a JIRA issue. // Typical types are "Normal", "Moderate", "Urgent", ... type Priority struct { - Self string `json:"self,omitempty"` - IconURL string `json:"iconUrl,omitempty"` - Name string `json:"name,omitempty"` - ID string `json:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` } // Watches represents a type of how many user are "observing" a JIRA issue to track the status / updates. type Watches struct { - Self string `json:"self,omitempty"` - WatchCount int `json:"watchCount,omitempty"` - IsWatching bool `json:"isWatching,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + WatchCount int `json:"watchCount,omitempty" structs:"watchCount,omitempty"` + IsWatching bool `json:"isWatching,omitempty" structs:"isWatching,omitempty"` } // User represents a user who is this JIRA issue assigned to. type User struct { - Self string `json:"self,omitempty"` - Name string `json:"name,omitempty"` - Key string `json:"key,omitempty"` - EmailAddress string `json:"emailAddress,omitempty"` - AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"` - DisplayName string `json:"displayName,omitempty"` - Active bool `json:"active,omitempty"` - TimeZone string `json:"timeZone,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"` + AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"` + DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"` + Active bool `json:"active,omitempty" structs:"active,omitempty"` + TimeZone string `json:"timeZone,omitempty" structs:"timeZone,omitempty"` } // AvatarUrls represents different dimensions of avatars / images type AvatarUrls struct { - Four8X48 string `json:"48x48,omitempty"` - Two4X24 string `json:"24x24,omitempty"` - One6X16 string `json:"16x16,omitempty"` - Three2X32 string `json:"32x32,omitempty"` + Four8X48 string `json:"48x48,omitempty" structs:"48x48,omitempty"` + Two4X24 string `json:"24x24,omitempty" structs:"24x24,omitempty"` + One6X16 string `json:"16x16,omitempty" structs:"16x16,omitempty"` + Three2X32 string `json:"32x32,omitempty" structs:"32x32,omitempty"` } // Component represents a "component" of a JIRA issue. // Components can be user defined in every JIRA instance. type Component struct { - Self string `json:"self,omitempty"` - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` } // Status represents the current status of a JIRA issue. // Typical status are "Open", "In Progress", "Closed", ... // Status can be user defined in every JIRA instance. type Status struct { - Self string `json:"self"` - Description string `json:"description"` - IconURL string `json:"iconUrl"` - Name string `json:"name"` - ID string `json:"id"` - StatusCategory StatusCategory `json:"statusCategory"` + Self string `json:"self" structs:"self"` + Description string `json:"description" structs:"description"` + IconURL string `json:"iconUrl" structs:"iconUrl"` + Name string `json:"name" structs:"name"` + ID string `json:"id" structs:"id"` + StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"` } // StatusCategory represents the category a status belongs to. // Those categories can be user defined in every JIRA instance. type StatusCategory struct { - Self string `json:"self"` - ID int `json:"id"` - Name string `json:"name"` - Key string `json:"key"` - ColorName string `json:"colorName"` + Self string `json:"self" structs:"self"` + ID int `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + Key string `json:"key" structs:"key"` + ColorName string `json:"colorName" structs:"colorName"` } // Progress represents the progress of a JIRA issue. type Progress struct { - Progress int `json:"progress"` - Total int `json:"total"` + Progress int `json:"progress" structs:"progress"` + Total int `json:"total" structs:"total"` } // Time represents the Time definition of JIRA as a time.Time of go @@ -195,29 +260,29 @@ type Time time.Time // Wrapper struct for search result type transitionResult struct { - Transitions []Transition `json:"transitions"` + Transitions []Transition `json:"transitions" structs:"transitions"` } // Transition represents an issue transition in JIRA type Transition struct { - ID string `json:"id"` - Name string `json:"name"` - Fields map[string]TransitionField `json:"fields"` + ID string `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + Fields map[string]TransitionField `json:"fields" structs:"fields"` } // TransitionField represents the value of one Transistion type TransitionField struct { - Required bool `json:"required"` + Required bool `json:"required" structs:"required"` } // CreateTransitionPayload is used for creating new issue transitions type CreateTransitionPayload struct { - Transition TransitionPayload `json:"transition"` + Transition TransitionPayload `json:"transition" structs:"transition"` } // TransitionPayload represents the request payload of Transistion calls like DoTransition type TransitionPayload struct { - ID string `json:"id"` + ID string `json:"id" structs:"id"` } // UnmarshalJSON will transform the JIRA time into a time.Time @@ -235,90 +300,90 @@ func (t *Time) UnmarshalJSON(b []byte) error { // One Worklog contains zero or n WorklogRecords // JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html type Worklog struct { - StartAt int `json:"startAt"` - MaxResults int `json:"maxResults"` - Total int `json:"total"` - Worklogs []WorklogRecord `json:"worklogs"` + StartAt int `json:"startAt" structs:"startAt"` + MaxResults int `json:"maxResults" structs:"maxResults"` + Total int `json:"total" structs:"total"` + Worklogs []WorklogRecord `json:"worklogs" structs:"worklogs"` } // WorklogRecord represents one entry of a Worklog type WorklogRecord struct { - Self string `json:"self"` - Author User `json:"author"` - UpdateAuthor User `json:"updateAuthor"` - Comment string `json:"comment"` - Created Time `json:"created"` - Updated Time `json:"updated"` - Started Time `json:"started"` - TimeSpent string `json:"timeSpent"` - TimeSpentSeconds int `json:"timeSpentSeconds"` - ID string `json:"id"` - IssueID string `json:"issueId"` + Self string `json:"self" structs:"self"` + Author User `json:"author" structs:"author"` + UpdateAuthor User `json:"updateAuthor" structs:"updateAuthor"` + Comment string `json:"comment" structs:"comment"` + Created Time `json:"created" structs:"created"` + Updated Time `json:"updated" structs:"updated"` + Started Time `json:"started" structs:"started"` + TimeSpent string `json:"timeSpent" structs:"timeSpent"` + TimeSpentSeconds int `json:"timeSpentSeconds" structs:"timeSpentSeconds"` + ID string `json:"id" structs:"id"` + IssueID string `json:"issueId" structs:"issueId"` } // Subtasks represents all issues of a parent issue. type Subtasks struct { - ID string `json:"id"` - Key string `json:"key"` - Self string `json:"self"` - Fields IssueFields `json:"fields"` + ID string `json:"id" structs:"id"` + Key string `json:"key" structs:"key"` + Self string `json:"self" structs:"self"` + Fields IssueFields `json:"fields" structs:"fields"` } // IssueLink represents a link between two issues in JIRA. type IssueLink struct { - ID string `json:"id,omitempty"` - Self string `json:"self,omitempty"` - Type IssueLinkType `json:"type"` - OutwardIssue *Issue `json:"outwardIssue"` - InwardIssue *Issue `json:"inwardIssue"` - Comment *Comment `json:"comment,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Type IssueLinkType `json:"type" structs:"type"` + OutwardIssue *Issue `json:"outwardIssue" structs:"outwardIssue"` + InwardIssue *Issue `json:"inwardIssue" structs:"inwardIssue"` + Comment *Comment `json:"comment,omitempty" structs:"comment,omitempty"` } // IssueLinkType represents a type of a link between to issues in JIRA. // Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc. type IssueLinkType struct { - ID string `json:"id,omitempty"` - Self string `json:"self,omitempty"` - Name string `json:"name"` - Inward string `json:"inward"` - Outward string `json:"outward"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name" structs:"name"` + Inward string `json:"inward" structs:"inward"` + Outward string `json:"outward" structs:"outward"` } // Comments represents a list of Comment. type Comments struct { - Comments []*Comment `json:"comments,omitempty"` + Comments []*Comment `json:"comments,omitempty" structs:"comments,omitempty"` } // Comment represents a comment by a person to an issue in JIRA. type Comment struct { - ID string `json:"id,omitempty"` - Self string `json:"self,omitempty"` - Name string `json:"name,omitempty"` - Author User `json:"author,omitempty"` - Body string `json:"body,omitempty"` - UpdateAuthor User `json:"updateAuthor,omitempty"` - Updated string `json:"updated,omitempty"` - Created string `json:"created,omitempty"` - Visibility CommentVisibility `json:"visibility,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + Name string `json:"name,omitempty" structs:name,omitempty"` + Author User `json:"author,omitempty" structs:"author,omitempty"` + Body string `json:"body,omitempty" structs:"body,omitempty"` + UpdateAuthor User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"` + Updated string `json:"updated,omitempty" structs:"updated,omitempty"` + Created string `json:"created,omitempty" structs:"created,omitempty"` + Visibility CommentVisibility `json:"visibility,omitempty" structs:"visibility,omitempty"` } // FixVersion represents a software release in which an issue is fixed. type FixVersion struct { - Archived *bool `json:"archived,omitempty"` - ID string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - ProjectID int `json:"projectId,omitempty"` - ReleaseDate string `json:"releaseDate,omitempty"` - Released *bool `json:"released,omitempty"` - Self string `json:"self,omitempty"` - UserReleaseDate string `json:"userReleaseDate,omitempty"` + Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` + ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"` + ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"` + Released *bool `json:"released,omitempty" structs:"released,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"` } // CommentVisibility represents he visibility of a comment. // E.g. Type could be "role" and Value "Administrators" type CommentVisibility struct { - Type string `json:"type,omitempty"` - Value string `json:"value,omitempty"` + Type string `json:"type,omitempty" structs:"type,omitempty"` + Value string `json:"value,omitempty" structs:"value,omitempty"` } // SearchOptions specifies the optional parameters to various List methods that @@ -337,10 +402,10 @@ type SearchOptions struct { // searchResult is only a small wrapper arround the Search (with JQL) method // to be able to parse the results type searchResult struct { - Issues []Issue `json:"issues"` - StartAt int `json:"startAt"` - MaxResults int `json:"maxResults"` - Total int `json:"total"` + Issues []Issue `json:"issues" structs:"issues"` + StartAt int `json:"startAt" structs:"startAt"` + MaxResults int `json:"maxResults" structs:"maxResults"` + Total int `json:"total" structs:"total"` } // CustomFields represents custom fields of JIRA diff --git a/issue_test.go b/issue_test.go index 1cead6f..0772f6e 100644 --- a/issue_test.go +++ b/issue_test.go @@ -8,6 +8,8 @@ import ( "reflect" "strings" "testing" + + "github.com/trivago/tgo/tcontainer" ) func TestIssueService_Get_Success(t *testing.T) { @@ -465,3 +467,128 @@ func TestIssueService_DoTransition(t *testing.T) { t.Errorf("Got error: %v", err) } } + +func TestIssueFields_TestMarshalJSON_PopulateUnknownsSuccess(t *testing.T) { + data := `{ + "customfield_123":"test", + "description":"example bug report", + "project":{ + "self":"http://www.example.com/jira/rest/api/2/project/EX", + "id":"10000", + "key":"EX", + "name":"Example", + "avatarUrls":{ + "48x48":"http://www.example.com/jira/secure/projectavatar?size=large&pid=10000", + "24x24":"http://www.example.com/jira/secure/projectavatar?size=small&pid=10000", + "16x16":"http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000", + "32x32":"http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000" + }, + "projectCategory":{ + "self":"http://www.example.com/jira/rest/api/2/projectCategory/10000", + "id":"10000", + "name":"FIRST", + "description":"First Project Category" + } + }, + "issuelinks":[ + { + "id":"10001", + "type":{ + "id":"10000", + "name":"Dependent", + "inward":"depends on", + "outward":"is depended by" + }, + "outwardIssue":{ + "id":"10004L", + "key":"PRJ-2", + "self":"http://www.example.com/jira/rest/api/2/issue/PRJ-2", + "fields":{ + "status":{ + "iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png", + "name":"Open" + } + } + } + }, + { + "id":"10002", + "type":{ + "id":"10000", + "name":"Dependent", + "inward":"depends on", + "outward":"is depended by" + }, + "inwardIssue":{ + "id":"10004", + "key":"PRJ-3", + "self":"http://www.example.com/jira/rest/api/2/issue/PRJ-3", + "fields":{ + "status":{ + "iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png", + "name":"Open" + } + } + } + } + ] + + }` + + i := new(IssueFields) + err := json.Unmarshal([]byte(data), i) + if err != nil { + t.Errorf("Expected nil error, recieved %s", err) + } + + if len(i.Unknowns) != 1 { + t.Errorf("Expected 1 unknown field to be present, recieved %d", len(i.Unknowns)) + } + if i.Description != "example bug report" { + t.Errorf("Expected description to be \"%s\", recieved \"%s\"", "example bug report", i.Description) + } + +} + +func TestIssueFields_MarshalJSON_Success(t *testing.T) { + /* + { + "customfield_123":"test", + "description":"example bug report", + "project":{ + "self":"http://www.example.com/jira/rest/api/2/project/EX", + "id":"10000", + "key":"EX" + } + } + */ + + i := &IssueFields{ + Description: "example bug report", + Unknowns: tcontainer.MarshalMap{ + "customfield_123": "test", + }, + Project: Project{ + Self: "http://www.example.com/jira/rest/api/2/project/EX", + ID: "10000", + Key: "EX", + }, + } + + bytes, err := json.Marshal(i) + if err != nil { + t.Errorf("Expected nil err, recieved %s", err) + } + + recieved := new(IssueFields) + // the order of json might be different. so unmarshal it again and comapre objects + err = json.Unmarshal(bytes, recieved) + if err != nil { + t.Errorf("Expected nil err, recieved %s", err) + } + + if !reflect.DeepEqual(i, recieved) { + t.Errorf("Recieved object different from expected") + } + +} diff --git a/metaissue.go b/metaissue.go index 620ebac..dfca209 100644 --- a/metaissue.go +++ b/metaissue.go @@ -2,6 +2,7 @@ package jira import ( "fmt" + "github.com/trivago/tgo/tcontainer" "strings" ) @@ -29,14 +30,14 @@ type MetaProject struct { // expect these for a general way. This will be returning a map. // Further processing must be done depending on what is required. type MetaIssueTypes struct { - Self string `json:"expand,omitempty"` - Id string `json:"id,omitempty"` - Description string `json:"description,omitempty"` - IconUrl string `json:"iconurl,omitempty"` - Name string `json:"name,omitempty"` - Subtasks bool `json:"subtask,omitempty"` - Expand string `json:"expand,omitempty"` - Fields map[string]interface{} `json:"fields,omitempty"` + Self string `json:"expand,omitempty"` + Id string `json:"id,omitempty"` + Description string `json:"description,omitempty"` + IconUrl string `json:"iconurl,omitempty"` + Name string `json:"name,omitempty"` + Subtasks bool `json:"subtask,omitempty"` + Expand string `json:"expand,omitempty"` + Fields tcontainer.MarshalMap `json:"fields,omitempty"` } // GetCreateMeta makes the api call to get the meta information required to create a ticket @@ -98,24 +99,35 @@ func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueTypes { // } // the returned map would have "Epic Link" as the key and "customfield_10806" as value. // This choice has been made so that the it is easier to generate the create api request later. -func (t *MetaIssueTypes) GetMandatoryFields() map[string]string { +func (t *MetaIssueTypes) GetMandatoryFields() (map[string]string, error) { ret := make(map[string]string) - for key, obj := range t.Fields { - details := obj.(map[string]interface{}) - if details["required"] == true { - ret[details["name"].(string)] = key + for key, _ := range t.Fields { + required, err := t.Fields.Bool(key + "/required") + if err != nil { + return nil, err + } + if required { + name, err := t.Fields.String(key + "/name") + if err != nil { + return nil, err + } + ret[name] = key } } - return nil + return ret, nil } // GetAllFields returns a map of all the fields for an IssueType. This includes all required and not required. // The key of the returned map is what you see in the form and the value is how it is representated in the jira schema. -func (t *MetaIssueTypes) GetAllFields() map[string]string { +func (t *MetaIssueTypes) GetAllFields() (map[string]string, error) { ret := make(map[string]string) - for key, obj := range t.Fields { - details := obj.(map[string]interface{}) - ret[details["name"].(string)] = key + for key, _ := range t.Fields { + + name, err := t.Fields.String(key + "/name") + if err != nil { + return nil, err + } + ret[name] = key } - return ret + return ret, nil } diff --git a/metaissue_test.go b/metaissue_test.go index 9644145..03509ec 100644 --- a/metaissue_test.go +++ b/metaissue_test.go @@ -377,3 +377,68 @@ func TestIssueService_GetCreateMeta_Success(t *testing.T) { } } + +func TestMetaIssueTypes_GetMandatoryFields(t *testing.T) { + data := make(map[string]interface{}) + + data["summary"] = map[string]interface{}{ + "required": true, + "name": "Summary", + } + + data["components"] = map[string]interface{}{ + "required": true, + "name": "Components", + } + + data["epicLink"] = map[string]interface{}{ + "required": false, + "name": "Epic Link", + } + + m := new(MetaIssueTypes) + m.Fields = data + + mandatory, err := m.GetMandatoryFields() + if err != nil { + t.Errorf("Expected nil error, recieved %s", err) + } + + if len(mandatory) != 2 { + t.Errorf("Expected 2 recieved %d", mandatory) + } + +} + +func TestMetaIssueTypes_GetAllFields(t *testing.T) { + data := make(map[string]interface{}) + + data["summary"] = map[string]interface{}{ + "required": true, + "name": "Summary", + } + + data["components"] = map[string]interface{}{ + "required": true, + "name": "Components", + } + + data["epicLink"] = map[string]interface{}{ + "required": false, + "name": "Epic Link", + } + + m := new(MetaIssueTypes) + m.Fields = data + + mandatory, err := m.GetAllFields() + + if err != nil { + t.Errorf("Expected nil err, recieved %s", err) + } + + if len(mandatory) != 3 { + t.Errorf("Expected 3 recieved %d", mandatory) + } + +} diff --git a/project.go b/project.go index 77b7e15..24ca6db 100644 --- a/project.go +++ b/project.go @@ -13,72 +13,72 @@ type ProjectService struct { // ProjectList represent a list of Projects type ProjectList []struct { - Expand string `json:"expand"` - Self string `json:"self"` - ID string `json:"id"` - Key string `json:"key"` - Name string `json:"name"` - AvatarUrls AvatarUrls `json:"avatarUrls"` - ProjectTypeKey string `json:"projectTypeKey"` - ProjectCategory ProjectCategory `json:"projectCategory,omitempty"` + Expand string `json:"expand" structs:"expand"` + Self string `json:"self" structs:"self"` + ID string `json:"id" structs:"id"` + Key string `json:"key" structs:"key"` + Name string `json:"name" structs:"name"` + AvatarUrls AvatarUrls `json:"avatarUrls" structs:"avatarUrls"` + ProjectTypeKey string `json:"projectTypeKey" structs:"projectTypeKey"` + ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectsCategory,omitempty"` } // ProjectCategory represents a single project category type ProjectCategory struct { - Self string `json:"self"` - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` + Self string `json:"self" structs:"self"` + ID string `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + Description string `json:"description" structs:"description"` } // Project represents a JIRA Project. type Project struct { - Expand string `json:"expand,omitempty"` - Self string `json:"self,omitempty"` - ID string `json:"id,omitempty"` - Key string `json:"key,omitempty"` - Description string `json:"description,omitempty"` - Lead User `json:"lead,omitempty"` - Components []ProjectComponent `json:"components,omitempty"` - IssueTypes []IssueType `json:"issueTypes,omitempty"` - URL string `json:"url,omitempty"` - Email string `json:"email,omitempty"` - AssigneeType string `json:"assigneeType,omitempty"` - Versions []Version `json:"versions,omitempty"` - Name string `json:"name,omitempty"` + Expand string `json:"expand,omitempty" structs:"expand,omitempty"` + Self string `json:"self,omitempty" structs:"self,omitempty"` + ID string `json:"id,omitempty" structs:"id,omitempty"` + Key string `json:"key,omitempty" structs:"key,omitempty"` + Description string `json:"description,omitempty" structs:"description,omitempty"` + Lead User `json:"lead,omitempty" structs:"lead,omitempty"` + Components []ProjectComponent `json:"components,omitempty" structs:"components,omitempty"` + IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"` + URL string `json:"url,omitempty" structs:"url,omitempty"` + Email string `json:"email,omitempty" structs:"email,omitempty"` + AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"` + Versions []Version `json:"versions,omitempty" structs:"versions,omitempty"` + Name string `json:"name,omitempty" structs:"name,omitempty"` Roles struct { - Developers string `json:"Developers,omitempty"` - } `json:"roles,omitempty"` - AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"` - ProjectCategory ProjectCategory `json:"projectCategory,omitempty"` + Developers string `json:"Developers,omitempty" structs:"Developers,omitempty"` + } `json:"roles,omitempty" structs:"roles,omitempty"` + AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"` + ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectCategory,omitempty"` } // Version represents a single release version of a project type Version struct { - Self string `json:"self"` - ID string `json:"id"` - Name string `json:"name"` - Archived bool `json:"archived"` - Released bool `json:"released"` - ReleaseDate string `json:"releaseDate"` - UserReleaseDate string `json:"userReleaseDate"` - ProjectID int `json:"projectId"` // Unlike other IDs, this is returned as a number + Self string `json:"self" structs:"self"` + ID string `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + Archived bool `json:"archived" structs:"archived"` + Released bool `json:"released" structs:"released"` + ReleaseDate string `json:"releaseDate" structs:"releaseDate"` + UserReleaseDate string `json:"userReleaseDate" structs:"userReleaseDate"` + ProjectID int `json:"projectId" structs:"projectId"` // Unlike other IDs, this is returned as a number } // ProjectComponent represents a single component of a project type ProjectComponent struct { - Self string `json:"self"` - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - Lead User `json:"lead"` - AssigneeType string `json:"assigneeType"` - Assignee User `json:"assignee"` - RealAssigneeType string `json:"realAssigneeType"` - RealAssignee User `json:"realAssignee"` - IsAssigneeTypeValid bool `json:"isAssigneeTypeValid"` - Project string `json:"project"` - ProjectID int `json:"projectId"` + Self string `json:"self" structs:"self"` + ID string `json:"id" structs:"id"` + Name string `json:"name" structs:"name"` + Description string `json:"description" structs:"description"` + Lead User `json:"lead" structs:"lead"` + AssigneeType string `json:"assigneeType" structs:"assigneeType"` + Assignee User `json:"assignee" structs:"assignee"` + RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType"` + RealAssignee User `json:"realAssignee" structs:"realAssignee"` + IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid"` + Project string `json:"project" structs:"project"` + ProjectID int `json:"projectId" structs:"projectId"` } // GetList gets all projects form JIRA From f2937d6875c9f37de0b6042c4d2113a34fc38903 Mon Sep 17 00:00:00 2001 From: Bidesh Thapaliya Date: Mon, 26 Sep 2016 17:59:51 +0200 Subject: [PATCH 4/7] Omit more empty attributes when converting from struct to map --- project.go | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/project.go b/project.go index 24ca6db..8dfccf2 100644 --- a/project.go +++ b/project.go @@ -25,10 +25,10 @@ type ProjectList []struct { // ProjectCategory represents a single project category type ProjectCategory struct { - Self string `json:"self" structs:"self"` - ID string `json:"id" structs:"id"` - Name string `json:"name" structs:"name"` - Description string `json:"description" structs:"description"` + Self string `json:"self" structs:"self,omitempty"` + ID string `json:"id" structs:"id,omitempty"` + Name string `json:"name" structs:"name,omitempty"` + Description string `json:"description" structs:"description,omitempty"` } // Project represents a JIRA Project. @@ -55,30 +55,30 @@ type Project struct { // Version represents a single release version of a project type Version struct { - Self string `json:"self" structs:"self"` - ID string `json:"id" structs:"id"` - Name string `json:"name" structs:"name"` - Archived bool `json:"archived" structs:"archived"` - Released bool `json:"released" structs:"released"` - ReleaseDate string `json:"releaseDate" structs:"releaseDate"` - UserReleaseDate string `json:"userReleaseDate" structs:"userReleaseDate"` - ProjectID int `json:"projectId" structs:"projectId"` // Unlike other IDs, this is returned as a number + Self string `json:"self" structs:"self,omitempty"` + ID string `json:"id" structs:"id,omitempty"` + Name string `json:"name" structs:"name,omitempty"` + Archived bool `json:"archived" structs:"archived,omitempty"` + Released bool `json:"released" structs:"released,omitempty"` + ReleaseDate string `json:"releaseDate" structs:"releaseDate,omitempty"` + UserReleaseDate string `json:"userReleaseDate" structs:"userReleaseDate,omitempty"` + ProjectID int `json:"projectId" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number } // ProjectComponent represents a single component of a project type ProjectComponent struct { - Self string `json:"self" structs:"self"` - ID string `json:"id" structs:"id"` - Name string `json:"name" structs:"name"` - Description string `json:"description" structs:"description"` - Lead User `json:"lead" structs:"lead"` - AssigneeType string `json:"assigneeType" structs:"assigneeType"` - Assignee User `json:"assignee" structs:"assignee"` - RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType"` - RealAssignee User `json:"realAssignee" structs:"realAssignee"` - IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid"` - Project string `json:"project" structs:"project"` - ProjectID int `json:"projectId" structs:"projectId"` + Self string `json:"self" structs:"self,omitempty"` + ID string `json:"id" structs:"id,omitempty"` + Name string `json:"name" structs:"name,omitempty"` + Description string `json:"description" structs:"description,omitempty"` + Lead User `json:"lead,omitempty" structs:"lead,omitempty"` + AssigneeType string `json:"assigneeType" structs:"assigneeType,omitempty"` + Assignee User `json:"assignee" structs:"assignee,omitempty"` + RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType,omitempty"` + RealAssignee User `json:"realAssignee" structs:"realAssignee,omitempty"` + IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid,omitempty"` + Project string `json:"project" structs:"project,omitempty"` + ProjectID int `json:"projectId" structs:"projectId,omitempty"` } // GetList gets all projects form JIRA From 435fdb84eb1f5e0325e00871dae8dd93af04262f Mon Sep 17 00:00:00 2001 From: Bidesh Thapaliya Date: Tue, 27 Sep 2016 11:54:09 +0200 Subject: [PATCH 5/7] Adds test for GetProjectWithName and GetIssueTypeWithName --- metaissue_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/metaissue_test.go b/metaissue_test.go index 03509ec..0fdf995 100644 --- a/metaissue_test.go +++ b/metaissue_test.go @@ -442,3 +442,31 @@ func TestMetaIssueTypes_GetAllFields(t *testing.T) { } } + +func TestCreateMetaInfo_GetProjectName_Success(t *testing.T) { + metainfo := new(CreateMetaInfo) + metainfo.Projects = append(metainfo.Projects, &MetaProject{ + Name: "SOP", + }) + + project := metainfo.GetProjectWithName("SOP") + if project == nil { + t.Errorf("Expected non nil value, recieved nil") + return + } + +} + +func TestMetaProject_GetIssueTypeWithName_CaseMismatch_Success(t *testing.T) { + m := new(MetaProject) + m.IssueTypes = append(m.IssueTypes, &MetaIssueTypes{ + Name: "Bug", + }) + + issuetype := m.GetIssueTypeWithName("BUG") + + if issuetype == nil { + t.Errorf("Expected non nil value, recieved nil") + return + } +} From d3ec8f16c04399fb5968aeade67345a768a2f039 Mon Sep 17 00:00:00 2001 From: Bidesh Thapaliya Date: Tue, 27 Sep 2016 12:18:30 +0200 Subject: [PATCH 6/7] Removes check for statuscode as jiraclient already does it. Adds test for nonok status code returned --- authentication.go | 3 --- authentication_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/authentication.go b/authentication.go index 59fdf88..e326779 100644 --- a/authentication.go +++ b/authentication.go @@ -133,9 +133,6 @@ func (s *AuthenticationService) GetCurrentUser() (*Session, error) { if err != nil { 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) - } defer resp.Body.Close() ret := new(Session) diff --git a/authentication_test.go b/authentication_test.go index 2f0426b..2d1c593 100644 --- a/authentication_test.go +++ b/authentication_test.go @@ -86,6 +86,43 @@ func TestAuthenticationService_Authenticated(t *testing.T) { } } +func TestAuthenticationService_GetUserInfo_NonOkStatusCode_Fail(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + testMethod(t, r, "POST") + testRequestURL(t, r, "/rest/auth/1/session") + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Error in read body: %s", err) + } + if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + t.Error("No username found") + } + if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + t.Error("No password found") + } + + fmt.Fprint(w, `{"session":{"name":"JSESSIONID","value":"12345678901234567890"},"loginInfo":{"failedLoginCount":10,"loginCount":127,"lastFailedLoginTime":"2016-03-16T04:22:35.386+0000","previousLoginTime":"2016-03-16T04:22:35.386+0000"}}`) + } + + if r.Method == "GET" { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/auth/1/session") + w.WriteHeader(http.StatusForbidden) + } + }) + + testClient.Authentication.AcquireSessionCookie("foo", "bar") + + _, err := testClient.Authentication.GetCurrentUser() + if err == nil { + t.Errorf("Non nil error expect, recieved nil") + } +} + func TestAuthenticationService_GetUserInfo_FailWithoutLogin(t *testing.T) { // no setup() required here testClient = new(Client) From e75e7750f2d2944a824cd8f3a2b65b35fc227c66 Mon Sep 17 00:00:00 2001 From: Bidesh Thapaliya Date: Tue, 27 Sep 2016 13:26:07 +0200 Subject: [PATCH 7/7] Adds test for authentication on expected json. Adds test to metaissue --- authentication.go | 3 +++ authentication_test.go | 43 ++++++++++++++++++++++++++++++++--- metaissue_test.go | 51 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/authentication.go b/authentication.go index e326779..59fdf88 100644 --- a/authentication.go +++ b/authentication.go @@ -133,6 +133,9 @@ func (s *AuthenticationService) GetCurrentUser() (*Session, error) { if err != nil { 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) + } defer resp.Body.Close() ret := new(Session) diff --git a/authentication_test.go b/authentication_test.go index 2d1c593..89e60ca 100644 --- a/authentication_test.go +++ b/authentication_test.go @@ -86,6 +86,44 @@ func TestAuthenticationService_Authenticated(t *testing.T) { } } +func TestAithenticationService_GetUserInfo_AccessForbidden_Fail(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + testMethod(t, r, "POST") + testRequestURL(t, r, "/rest/auth/1/session") + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Errorf("Error in read body: %s", err) + } + if bytes.Index(b, []byte(`"username":"foo"`)) < 0 { + t.Error("No username found") + } + if bytes.Index(b, []byte(`"password":"bar"`)) < 0 { + t.Error("No password found") + } + + fmt.Fprint(w, `{"session":{"name":"JSESSIONID","value":"12345678901234567890"},"loginInfo":{"failedLoginCount":10,"loginCount":127,"lastFailedLoginTime":"2016-03-16T04:22:35.386+0000","previousLoginTime":"2016-03-16T04:22:35.386+0000"}}`) + } + + if r.Method == "GET" { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/auth/1/session") + + w.WriteHeader(http.StatusForbidden) + } + }) + + testClient.Authentication.AcquireSessionCookie("foo", "bar") + + _, err := testClient.Authentication.GetCurrentUser() + if err == nil { + t.Errorf("Non nil error expect, recieved nil") + } + +} + func TestAuthenticationService_GetUserInfo_NonOkStatusCode_Fail(t *testing.T) { setup() defer teardown() @@ -111,7 +149,8 @@ func TestAuthenticationService_GetUserInfo_NonOkStatusCode_Fail(t *testing.T) { if r.Method == "GET" { testMethod(t, r, "GET") testRequestURL(t, r, "/rest/auth/1/session") - w.WriteHeader(http.StatusForbidden) + //any status but 200 + w.WriteHeader(240) } }) @@ -233,10 +272,8 @@ func TestAuthenticationService_Logout_FailWithoutLogin(t *testing.T) { } }) - err := testClient.Authentication.Logout() if err == nil { t.Error("Expected not nil, got nil") } - } diff --git a/metaissue_test.go b/metaissue_test.go index 0fdf995..0f53e23 100644 --- a/metaissue_test.go +++ b/metaissue_test.go @@ -410,6 +410,40 @@ func TestMetaIssueTypes_GetMandatoryFields(t *testing.T) { } +func TestMetaIssueTypes_GetMandatoryFields_NonExistentRequiredKey_Fail(t *testing.T) { + data := make(map[string]interface{}) + + data["summary"] = map[string]interface{}{ + "name": "Summary", + } + + m := new(MetaIssueTypes) + m.Fields = data + + _, err := m.GetMandatoryFields() + if err == nil { + t.Error("Expected non nil errpr, recieved nil") + } + +} + +func TestMetaIssueTypes_GetMandatoryFields_NonExistentNameKey_Fail(t *testing.T) { + data := make(map[string]interface{}) + + data["summary"] = map[string]interface{}{ + "required": true, + } + + m := new(MetaIssueTypes) + m.Fields = data + + _, err := m.GetMandatoryFields() + if err == nil { + t.Error("Expected non nil errpr, recieved nil") + } + +} + func TestMetaIssueTypes_GetAllFields(t *testing.T) { data := make(map[string]interface{}) @@ -443,6 +477,23 @@ func TestMetaIssueTypes_GetAllFields(t *testing.T) { } +func TestMetaIssueTypes_GetAllFields_NonExistingNameKey_Fail(t *testing.T) { + data := make(map[string]interface{}) + + data["summary"] = map[string]interface{}{ + "required": true, + } + + m := new(MetaIssueTypes) + m.Fields = data + + _, err := m.GetAllFields() + if err == nil { + t.Error("Expected non nil error, recieved nil") + } + +} + func TestCreateMetaInfo_GetProjectName_Success(t *testing.T) { metainfo := new(CreateMetaInfo) metainfo.Projects = append(metainfo.Projects, &MetaProject{