diff --git a/README.md b/README.md index 793049f..3f262ef 100644 --- a/README.md +++ b/README.md @@ -88,10 +88,18 @@ Zentao API client enabling Go programs to interact with Zentao in a simple and u - [x] 修改任务 - [x] Bug - [x] 获取产品Bug列表 + - [x] 获取项目Bug列表 + - [x] 获取执行Bug列表 - [x] 获取Bug详情 - [x] 创建Bug - [x] 删除Bug - [x] 修改Bug + - [x] 关闭Bug + - [x] 指派Bug + - [x] 确认Bug + - [x] 解决Bug + - [x] 激活Bug + - [ ] 评估Bug(开源版不支持) - [x] 版本(Builds) - [x] 获取项目版本列表 - [x] 获取执行版本详情 @@ -140,7 +148,4 @@ import "github.com/easysoft/go-zentao/v20/zentao" docker compose -f hack/docker-compose.yml up -d ``` -## TODO - -- [ ] 优化代码 & 添加单元测试 diff --git a/example/bugs/main.go b/example/bugs/main.go new file mode 100644 index 0000000..b575a3c --- /dev/null +++ b/example/bugs/main.go @@ -0,0 +1,161 @@ +// +// Copyright 2024, easysoft +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package main + +import ( + "fmt" + "log" + + "github.com/easysoft/go-zentao/v20/zentao" +) + +func main() { + zt, err := zentao.NewBasicAuthClient( + "admin", + "jaege1ugh4ooYip7", + zentao.WithBaseURL("http://127.0.0.1"), + zentao.WithDevMode(), + zentao.WithDumpAll(), + zentao.WithoutProxy(), + ) + if err != nil { + panic(err) + } + p1, _, err := zt.Bugs.ListByProducts(1, zentao.ListOptions{ + Page: 1, + Limit: 1, + // Status: "all", // 列出所有 + }) + if err != nil { + panic(err) + } + log.Printf("product 1 bugs count: %v", len(p1.Bugs)) + p2, _, err := zt.Bugs.ListByProjects(2, zentao.ListOptions{Page: 1, Limit: 1}) + if err != nil { + panic(err) + } + log.Printf("project 1 bugs count: %v", len(p2.Bugs)) + p3, _, err := zt.Bugs.ListByExecutions(1, zentao.ListOptions{Page: 1, Limit: 1}) + if err != nil { + panic(err) + } + log.Printf("execution 1 bugs count: %v", len(p3.Bugs)) + p4, _, err := zt.Bugs.GetByID(p3.Bugs[0].ID) + if err != nil { + panic(err) + } + log.Printf("bug 1: %v", p4.Title) + p5, _, err := zt.Bugs.Create(1, zentao.BugCreateMeta{ + BugMeta: zentao.BugMeta{ + Title: fmt.Sprintf("go sdk bug %v", p1.Total+1), + Module: 1, + Type: zentao.CodeErrorBugType, + Severity: 1, + Pri: 1, + Project: 2, + Story: 1, + Execution: 3, + Task: 6, + Steps: "
[步骤]
[结果]
[期望]
", + AssignedTo: "productManager", + FeedbackBy: "zhangsan", + }, + AllBuilds: "on", + Openedbuild: []string{"trunk"}, // 主干 + }) + if err != nil { + panic(err) + } + log.Printf("product 1 create bug: %v", p5.Title) + p51, _, err := zt.Bugs.UpdateByID(p5.ID, zentao.BugUpdateMeta{ + Title: fmt.Sprintf("fake go sdk bug %v", p5.ID), + }) + if err != nil { + panic(err) + } + log.Printf("product 1 update bug: %v", p51.Title) + p6, _, err := zt.Bugs.AssignByID(p5.ID, zentao.AssignBug{ + AssignedTo: "admin", + Comment: "go sdk assign bug", + }) + if err != nil { + panic(err) + } + log.Printf("product 1 assign bug: %v", p6.Title) + p7, _, err := zt.Bugs.ConfirmByID(p5.ID, zentao.ConfirmBug{ + AssignBug: zentao.AssignBug{ + AssignedTo: "admin", + Comment: "go sdk confirm bug", + }, + Pri: 1, + Type: zentao.CodeErrorBugType, + Status: zentao.ActiveBugStatus, + }) + if err != nil { + panic(err) + } + log.Printf("product 1 confirm bug: %v", p7.Title) + p8, _, err := zt.Bugs.ResolveByID(p5.ID, zentao.ResolveBug{ + Resolution: zentao.BugExternal, + Comment: "go sdk resolve bug", + }) + if err != nil { + panic(err) + } + log.Printf("product 1 resolve bug: %v", p8.Title) + p9, _, err := zt.Bugs.ActiveByID(p5.ID, zentao.ActiveBug{ + Comment: "go sdk active bug", + AssignedTo: "admin", + OpenedBuild: []string{"trunk", "2"}, + }) + if err != nil { + panic(err) + } + log.Printf("product 1 active bug: %v", p9.Title) + _, _, err = zt.Bugs.ResolveByID(p5.ID, zentao.ResolveBug{ + Resolution: zentao.BugExternal, + Comment: "go sdk resolve2 bug", + }) + if err != nil { + panic(err) + } + p10, _, err := zt.Bugs.CloseByID(p5.ID, zentao.CommentBug{ + Comment: "go sdk close bug", + }) + if err != nil { + panic(err) + } + log.Printf("product 1 close bug: %v", p10.Title) + // p11, _, err := zt.Bugs.ListByProducts(1, zentao.ListOptions{ + // Page: 1, + // Limit: 100, + // Status: "all", // 列出所有 + // }) + // if err != nil { + // panic(err) + // } + // log.Printf("product 1 all bugs count: %v", len(p11.Bugs)) + // p12, _, err := zt.Bugs.ListByProducts(1, zentao.ListOptions{ + // Page: 1, + // Limit: 100, + // Status: "active", // 列出激活的 + // }) + // if err != nil { + // panic(err) + // } + // log.Printf("product 1 bugs count: %v", len(p12.Bugs)) +} diff --git a/zentao/bugs.go b/zentao/bugs.go index 0803808..f05d5cd 100644 --- a/zentao/bugs.go +++ b/zentao/bugs.go @@ -34,98 +34,187 @@ type ListProductsBugsMsg struct { } type BugMeta struct { - Branch int `json:"branch,omitempty"` - Module int `json:"module,omitempty"` - Execution int `json:"execution,omitempty"` - Os string `json:"os,omitempty"` - Browser string `json:"browser,omitempty"` - Task int `json:"task,omitempty"` - Story int `json:"story,omitempty"` - Deadline string `json:"deadline,omitempty"` - Title string `json:"title"` - Severity int `json:"severity"` - Pri int `json:"pri"` - Steps string `json:"steps"` - Type string `json:"type"` + Branch int `json:"branch,omitempty"` // 创建需求时, 该字段不写 + Module int `json:"module,omitempty"` + Os string `json:"os,omitempty"` + Browser string `json:"browser,omitempty"` + Task int `json:"task,omitempty"` + Story int `json:"story,omitempty"` + Deadline string `json:"deadline,omitempty"` + Title string `json:"title"` + Project int `json:"project"` + Execution int `json:"execution,omitempty"` + Severity int `json:"severity"` + Pri int `json:"pri"` + Steps string `json:"steps"` + Type BugType `json:"type"` + AssignedTo any `json:"assignedTo"` + FeedbackBy any `json:"feedbackBy,omitempty"` // 仅bug接口 } type BugBody struct { BugMeta - ID int `json:"id"` - Project int `json:"project"` - Product int `json:"product"` - Plan int `json:"plan"` - Storyversion int `json:"storyVersion"` - Totask int `json:"toTask"` - Tostory int `json:"toStory"` - Keywords string `json:"keywords"` - Hardware string `json:"hardware"` - Found string `json:"found"` - Status any `json:"status,omitempty"` // 列表返回结构体, 详情返回字符串 - Substatus string `json:"subStatus"` - Color string `json:"color"` - Confirmed int `json:"confirmed"` - Activatedcount int `json:"activatedCount"` - Entry string `json:"entry"` - Lines string `json:"lines"` - V1 string `json:"v1"` - V2 string `json:"v2"` - Duplicatebug int `json:"duplicateBug"` - Linkbug string `json:"linkBug"` - Case int `json:"case"` - Caseversion int `json:"caseVersion"` - Result int `json:"result"` - Repo int `json:"repo"` - Repotype string `json:"repoType"` - Testtask int `json:"testtask"` - Deleted string `json:"deleted"` - Activateddate string `json:"activatedDate"` - Feedbackby string `json:"feedbackBy,omitempty"` // 仅bug接口 - Notifyemail string `json:"notifyEmail,omitempty"` // 仅bug接口 - Mailto any `json:"mailto"` - Openedby any `json:"openedBy"` - Openeddate string `json:"openedDate"` - Openedbuild string `json:"openedBuild"` - Assignedto any `json:"assignedTo"` - Assigneddate any `json:"assignedDate"` - Resolvedby any `json:"resolvedBy"` - Resolution string `json:"resolution"` - Resolvedbuild string `json:"resolvedBuild"` - Resolveddate any `json:"resolvedDate"` - Closedby any `json:"closedBy"` - Closeddate string `json:"closedDate"` - Lasteditedby any `json:"lastEditedBy"` - Lastediteddate string `json:"lastEditedDate"` - Needconfirm bool `json:"needconfirm,omitempty"` // 仅列表返回 + ID int `json:"id"` + Product int `json:"product"` + Plan int `json:"plan"` + StoryVersion int `json:"storyVersion"` + PriOrder string `json:"priOrder"` + SeverityOrder int `json:"severityOrder"` + Totask int `json:"toTask"` + Tostory int `json:"toStory"` + Keywords string `json:"keywords"` + Hardware string `json:"hardware"` + Found string `json:"found"` + Status BugStatus `json:"status,omitempty"` + SubStatus string `json:"subStatus"` + Color string `json:"color"` + Confirmed int `json:"confirmed"` + ActivatedCount int `json:"activatedCount"` + ActivatedDate any `json:"activatedDate"` + Entry string `json:"entry"` + Lines string `json:"lines"` + V1 string `json:"v1"` + V2 string `json:"v2"` + DuplicateBug int `json:"duplicateBug"` + LinkBug string `json:"linkBug"` + Case int `json:"case"` + CaseVersion int `json:"caseVersion"` + Feedback int `json:"feedback"` + Result int `json:"result"` + Repo int `json:"repo"` + MR int `json:"mr"` + RepoType string `json:"repoType"` + IssueKey string `json:"issueKey"` + TestTask int `json:"testtask"` + Deleted any `json:"deleted"` // 字符串orbool + NotifyEmail string `json:"notifyEmail,omitempty"` // 仅bug接口 + Mailto any `json:"mailto"` + OpenedBy any `json:"openedBy"` + OpenedDate string `json:"openedDate"` + OpenedBuild any `json:"openedBuild"` + AssignedDate any `json:"assignedDate"` + ResolvedBy any `json:"resolvedBy"` + Resolution BugCloseReason `json:"resolution"` + ResolvedBuild string `json:"resolvedBuild"` + ResolvedDate any `json:"resolvedDate"` + ClosedBy any `json:"closedBy"` + ClosedDate string `json:"closedDate"` + LasteditedBy any `json:"lastEditedBy"` + LasteditedDate string `json:"lastEditedDate"` + NeedConfirm bool `json:"needconfirm,omitempty"` // 仅列表返回 + RelatedBug string `json:"relatedBug"` + StatusName string `json:"statusName"` + ProductStatus string `json:"productStatus"` } type BugCreateMeta struct { BugMeta + AllBuilds string `json:"allBuilds,omitempty"` // 默认为all,所有版本 Openedbuild []string `json:"openedBuild"` } +type BugUpdateMeta struct { + Title string `json:"title"` + Project int `json:"project,omitempty"` + Execution int `json:"execution,omitempty"` + Openedbuild any `json:"openedBuild,omitempty"` + AssignedTo any `json:"assignedTo,omitempty"` + Pri int `json:"pri,omitempty"` + Severity int `json:"severity,omitempty"` + Type BugType `json:"type,omitempty"` + Story int `json:"story,omitempty"` + ResolvedBy any `json:"resolvedBy,omitempty"` + ClosedBy any `json:"closedBy,omitempty"` + Resolution BugCloseReason `json:"resolution,omitempty"` + Product int `json:"product,omitempty"` + Plan int `json:"plan,omitempty"` + Task int `json:"task,omitempty"` + Module int `json:"module,omitempty"` + Steps string `json:"steps,omitempty"` + Mailto any `json:"os,omitempty"` + Keywords any `json:"keywords,omitempty"` +} + type BugGetMsg struct { BugBody - Executionname string `json:"executionName"` - Storytitle string `json:"storyTitle"` - Storystatus string `json:"storyStatus"` - Lateststoryversion int `json:"latestStoryVersion"` - Taskname interface{} `json:"taskName"` - Planname interface{} `json:"planName"` - Projectname string `json:"projectName"` - Tocases []interface{} `json:"toCases"` - Files []interface{} `json:"files"` -} - -func (s *BugsService) ListByProducts(id int64) (*ListProductsBugsMsg, *req.Response, error) { + ExecutionName string `json:"executionName"` + StoryTitle string `json:"storyTitle"` + StoryStatus string `json:"storyStatus"` + LatestStoryVersion any `json:"latestStoryVersion"` // 未关联需求时string, 关联需求时int + TaskName any `json:"taskName"` + PlanName any `json:"planName"` + ProjectName string `json:"projectName"` + ToCases any `json:"toCases"` + Files any `json:"files"` + LinkMRTitles any `json:"linkMRTitles"` + ModuleTitle string `json:"moduleTitle"` + Actions any `json:"actions"` + PreAndNext any `json:"preAndNext"` +} + +type AssignBug struct { + AssignedTo string `json:"assignedTo"` + Mailto any `json:"mailto,omitempty"` + Comment string `json:"comment,omitempty"` +} + +type ConfirmBug struct { + AssignBug + Pri int `json:"pri"` + Type BugType `json:"type"` + Status BugStatus `json:"status"` + Deadline string `json:"deadline,omitempty"` +} + +type ResolveBug struct { + Resolution BugCloseReason `json:"resolution"` + ResolvedBuild string `json:"resolvedBuild"` + ResolvedDate string `json:"resolvedDate,omitempty"` + DuplicateBug any `json:"duplicateBug,omitempty"` + AssignedTo string `json:"assignedTo"` + Comment string `json:"comment"` +} + +type ActiveBug struct { + AssignedTo string `json:"assignedTo,omitempty"` + Comment string `json:"comment,omitempty"` + OpenedBuild any `json:"openedBuild"` +} + +type CommentBug struct { + Comment string `json:"comment,omitempty"` +} + +func (s *BugsService) ListByProducts(id int64, op ListOptions) (*ListProductsBugsMsg, *req.Response, error) { var et ListProductsBugsMsg resp, err := s.client.client.R(). SetHeader("Token", s.client.token). + SetQueryString(op.toURLValues()). SetSuccessResult(&et). Get(s.client.RequestURL(fmt.Sprintf("/products/%d/bugs", id))) return &et, resp, err } +func (s *BugsService) ListByProjects(id int64, op ListOptions) (*ListProductsBugsMsg, *req.Response, error) { + var et ListProductsBugsMsg + resp, err := s.client.client.R(). + SetHeader("Token", s.client.token). + SetQueryString(op.toURLValues()). + SetSuccessResult(&et). + Get(s.client.RequestURL(fmt.Sprintf("/projects/%d/bugs", id))) + return &et, resp, err +} + +func (s *BugsService) ListByExecutions(id int64, op ListOptions) (*ListProductsBugsMsg, *req.Response, error) { + var et ListProductsBugsMsg + resp, err := s.client.client.R(). + SetHeader("Token", s.client.token). + SetQueryString(op.toURLValues()). + SetSuccessResult(&et). + Get(s.client.RequestURL(fmt.Sprintf("/executions/%d/bugs", id))) + return &et, resp, err +} + // Create 创建Bug func (s *BugsService) Create(id int, build BugCreateMeta) (*BugGetMsg, *req.Response, error) { var u BugGetMsg @@ -148,7 +237,7 @@ func (s *BugsService) DeleteByID(id int) (*CustomResp, *req.Response, error) { } // UpdateByID 修改Bug -func (s *BugsService) UpdateByID(id int, exec BugCreateMeta) (*BugGetMsg, *req.Response, error) { +func (s *BugsService) UpdateByID(id int, exec BugUpdateMeta) (*BugGetMsg, *req.Response, error) { var u BugGetMsg resp, err := s.client.client.R(). SetHeader("Token", s.client.token). @@ -167,3 +256,68 @@ func (s *BugsService) GetByID(id int) (*BugGetMsg, *req.Response, error) { Get(s.client.RequestURL(fmt.Sprintf("/bugs/%d", id))) return &u, resp, err } + +// CloseByID 关闭Bug(需要已经resolve) +func (s *BugsService) CloseByID(id int, comment CommentBug) (*BugGetMsg, *req.Response, error) { + var u BugGetMsg + resp, err := s.client.client.R(). + SetHeader("Token", s.client.token). + SetBody(&comment). + SetSuccessResult(&u). + Post(s.client.RequestURL(fmt.Sprintf("/bugs/%d/close", id))) + return &u, resp, err +} + +// AssignByID 指派Bug +func (s *BugsService) AssignByID(id int, assign AssignBug) (*BugGetMsg, *req.Response, error) { + var u BugGetMsg + resp, err := s.client.client.R(). + SetHeader("Token", s.client.token). + SetBody(&assign). + SetSuccessResult(&u). + Post(s.client.RequestURL(fmt.Sprintf("/bugs/%d/assign", id))) + return &u, resp, err +} + +// ConfirmByID 确认Bug +func (s *BugsService) ConfirmByID(id int, confirm ConfirmBug) (*BugGetMsg, *req.Response, error) { + var u BugGetMsg + resp, err := s.client.client.R(). + SetHeader("Token", s.client.token). + SetBody(&confirm). + SetSuccessResult(&u). + Post(s.client.RequestURL(fmt.Sprintf("/bugs/%d/confirm", id))) + return &u, resp, err +} + +// ResolveByID 解决Bug +func (s *BugsService) ResolveByID(id int, resolve ResolveBug) (*BugGetMsg, *req.Response, error) { + var u BugGetMsg + resp, err := s.client.client.R(). + SetHeader("Token", s.client.token). + SetBody(&resolve). + SetSuccessResult(&u). + Post(s.client.RequestURL(fmt.Sprintf("/bugs/%d/resolve", id))) + return &u, resp, err +} + +// ActiveByID 激活Bug +func (s *BugsService) ActiveByID(id int, active ActiveBug) (*BugGetMsg, *req.Response, error) { + var u BugGetMsg + resp, err := s.client.client.R(). + SetHeader("Token", s.client.token). + SetBody(&active). + SetSuccessResult(&u). + Post(s.client.RequestURL(fmt.Sprintf("/bugs/%d/active", id))) + return &u, resp, err +} + +// EstimateByID 评估Bug +func (s *BugsService) EstimateByID(id int) (*BugGetMsg, *req.Response, error) { + var u BugGetMsg + resp, err := s.client.client.R(). + SetHeader("Token", s.client.token). + SetSuccessResult(&u). + Get(s.client.RequestURL(fmt.Sprintf("/bugs/%d/estimate", id))) + return &u, resp, err +} diff --git a/zentao/types.go b/zentao/types.go index 416b527..dc24711 100644 --- a/zentao/types.go +++ b/zentao/types.go @@ -118,6 +118,40 @@ var ( LifeTimeNull ExecutionLifeTime = "" // 未定义 ) +type BugType string // Bug类型 + +var ( + CodeErrorBugType BugType = "codeerror" // 代码错误 + DesignDefectBugType BugType = "designdefect" // 设计缺陷 + ConfigBugType BugType = "config" // 配置相关 + InstallBugType BugType = "install" // 安装部署 + SecurityBugType BugType = "security" // 安全相关 + PerformanceBugType BugType = "performance" // 性能问题 + StandardBugType BugType = "standard" // 标准问题 + AutomationBugType BugType = "automation" // 测试脚本 + OtherBugType BugType = "other" // 其他 +) + +type BugStatus string // Bug状态 + +var ( + ActiveBugStatus BugStatus = "active" // 激活 + ClosedBugStatus BugStatus = "closed" // 关闭 + ResolvedBugStatus BugStatus = "resolved" // 已解决 +) + +type BugCloseReason string // Bug关闭原因 + +var ( + BugClose BugCloseReason = "done" // 完成 + BugDuplicate BugCloseReason = "duplicate" // 重复 + BugPostponed BugCloseReason = "postponed" // 延期 + BugWillNotDo BugCloseReason = "willnotdo" // 不做 + BugCancel BugCloseReason = "cancel" // 取消 + BugByDesign BugCloseReason = "bydesign" // 设计如此 + BugExternal BugCloseReason = "external" // 外部原因 +) + // CustomResp 通用Resp type CustomResp struct { Message string `json:"message,omitempty"` diff --git a/zentao/zentao.go b/zentao/zentao.go index 5f915a8..d40a183 100644 --- a/zentao/zentao.go +++ b/zentao/zentao.go @@ -171,3 +171,21 @@ func (c *Client) RequestURL(path string) string { u.Path = c.baseURL.Path + path return u.String() } + +// ListOptions specifies the optional parameters to various List methods that +type ListOptions struct { + Page int `url:"page,omitempty" json:"page,omitempty"` + Limit int `url:"limit,omitempty" json:"limit,omitempty"` + // Status string `url:"status,omitempty" json:"status,omitempty"` // 目前不生效 NeedFixed +} + +func (o ListOptions) toURLValues() string { + v := url.Values{} + if o.Page > 0 { + v.Set("page", fmt.Sprintf("%d", o.Page)) + } + if o.Limit > 0 { + v.Set("limit", fmt.Sprintf("%d", o.Limit)) + } + return v.Encode() +}