package youtrack_test import ( "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "task/internal/youtrack" ) func TestListIssues(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/issues" { t.Fatalf("path = %s", r.URL.Path) } if got := r.Header.Get("Authorization"); got != "Bearer tok" { t.Fatalf("auth = %q", got) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`[ { "idReadable": "PROJ-1", "summary": "Fix bug", "description": "details", "updated": 1700000000000, "customFields": [ {"name": "State", "$type": "StateIssueCustomField", "value": {"name": "In Progress", "$type": "StateBundleElement"}} ] } ]`)) })) defer srv.Close() c := &youtrack.Client{BaseURL: srv.URL, Token: "tok", HTTPClient: srv.Client()} issues, err := c.ListIssues(context.Background(), "project: PROJ") if err != nil { t.Fatalf("ListIssues: %v", err) } if len(issues) != 1 { t.Fatalf("len = %d", len(issues)) } iss := issues[0] if iss.IDReadable != "PROJ-1" || iss.Summary != "Fix bug" || iss.State != "In Progress" { t.Fatalf("issue = %+v", iss) } if c.IssueURL("PROJ-1") != srv.URL+"/issue/PROJ-1" { t.Fatalf("url = %s", c.IssueURL("PROJ-1")) } } func TestListIssuesHTTPError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "nope", http.StatusUnauthorized) })) defer srv.Close() c := &youtrack.Client{BaseURL: srv.URL, Token: "bad", HTTPClient: srv.Client()} _, err := c.ListIssues(context.Background(), "project: X") if err == nil { t.Fatal("expected error") } } func TestCreateAndUpdateIssueWithRenamedStateField(t *testing.T) { var statePosts []map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && r.URL.Path == "/api/admin/projects/PROJ": _, _ = w.Write([]byte(`{"id":"0-1","shortName":"PROJ"}`)) case r.Method == http.MethodPost && r.URL.Path == "/api/issues": body, _ := io.ReadAll(r.Body) var payload map[string]any if err := json.Unmarshal(body, &payload); err != nil { t.Fatalf("body: %v", err) } if _, hasCF := payload["customFields"]; hasCF { t.Fatal("create must not send customFields for State") } _, _ = w.Write([]byte(`{"idReadable":"PROJ-9","summary":"New","description":"body"}`)) case r.Method == http.MethodGet && strings.HasPrefix(r.URL.Path, "/api/issues/"): // Field is named "Status", not "State". _, _ = w.Write([]byte(`{ "idReadable":"PROJ-9","summary":"New","description":"body", "customFields":[{ "id":"92-1","name":"Status","$type":"StateIssueCustomField", "value":{"name":"Submitted"} }] }`)) case r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/api/issues/"): body, _ := io.ReadAll(r.Body) var payload map[string]any _ = json.Unmarshal(body, &payload) if cfs, ok := payload["customFields"].([]any); ok { statePosts = append(statePosts, cfs[0].(map[string]any)) w.WriteHeader(http.StatusOK) return } if payload["summary"] == "Updated" { _, _ = w.Write([]byte(`{"idReadable":"PROJ-9","summary":"Updated","description":"new"}`)) return } t.Fatalf("unexpected post body: %s", body) default: t.Fatalf("unexpected %s %s", r.Method, r.URL.Path) } })) defer srv.Close() c := &youtrack.Client{BaseURL: srv.URL, Token: "tok", HTTPClient: srv.Client()} created, err := c.CreateIssue(context.Background(), "PROJ", "New", "body", "Open") if err != nil { t.Fatalf("CreateIssue: %v", err) } if created.IDReadable != "PROJ-9" { t.Fatalf("created = %+v", created) } updated, err := c.UpdateIssue(context.Background(), "PROJ-9", "Updated", "new", "In Progress") if err != nil { t.Fatalf("UpdateIssue: %v", err) } if updated.Summary != "Updated" { t.Fatalf("updated = %+v", updated) } if len(statePosts) < 1 { t.Fatal("expected state field posts") } for _, p := range statePosts { if p["name"] != "Status" { t.Fatalf("expected field name Status, got %v", p["name"]) } if p["$type"] != "StateIssueCustomField" { t.Fatalf("$type = %v", p["$type"]) } } } func TestSetStateMachineEvent(t *testing.T) { var posted map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet: _, _ = w.Write([]byte(`{ "idReadable":"TAS-1","summary":"x", "customFields":[{ "id":"82-11","name":"Stage","$type":"StateMachineIssueCustomField", "value":{"name":"Open"}, "possibleEvents":[ {"id":"start","presentation":"Start progress","$type":"Event"}, {"id":"fix","presentation":"Fixed","$type":"Event"} ] }] }`)) case r.Method == http.MethodPost: body, _ := io.ReadAll(r.Body) _ = json.Unmarshal(body, &posted) w.WriteHeader(http.StatusOK) default: t.Fatalf("unexpected %s %s", r.Method, r.URL.Path) } })) defer srv.Close() c := &youtrack.Client{BaseURL: srv.URL, Token: "tok", HTTPClient: srv.Client()} if err := c.SetState(context.Background(), "TAS-1", "In Progress"); err != nil { t.Fatalf("SetState: %v", err) } cfs := posted["customFields"].([]any) cf := cfs[0].(map[string]any) if cf["name"] != "Stage" || cf["$type"] != "StateMachineIssueCustomField" { t.Fatalf("cf = %+v", cf) } ev := cf["event"].(map[string]any) if ev["id"] != "start" { t.Fatalf("event = %+v (want fuzzy match to Start progress)", ev) } } func TestToIssueReadsStatusField(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "idReadable":"TAS-1","summary":"x", "customFields":[{ "id":"1","name":"Status","$type":"StateIssueCustomField", "value":{"name":"In Progress"} }] }`)) })) defer srv.Close() c := &youtrack.Client{BaseURL: srv.URL, Token: "tok", HTTPClient: srv.Client()} iss, err := c.GetIssue(context.Background(), "TAS-1") if err != nil { t.Fatal(err) } if iss.State != "In Progress" { t.Fatalf("state = %q", iss.State) } } func TestCreateAndListWorkItems(t *testing.T) { var created map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/timeTracking/workItems"): _, _ = w.Write([]byte(`[{"id":"wi-1","date":1700000000000,"text":"x","duration":{"minutes":30}}]`)) case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/timeTracking/workItems"): body, _ := io.ReadAll(r.Body) _ = json.Unmarshal(body, &created) _, _ = w.Write([]byte(`{"id":"wi-2","date":1700000000000,"text":"note","duration":{"minutes":45}}`)) default: t.Fatalf("unexpected %s %s", r.Method, r.URL.Path) } })) defer srv.Close() c := &youtrack.Client{BaseURL: srv.URL, Token: "tok", HTTPClient: srv.Client()} list, err := c.ListWorkItems(context.Background(), "TAS-1") if err != nil || len(list) != 1 || list[0].Minutes != 30 { t.Fatalf("list = %+v err=%v", list, err) } wi, err := c.CreateWorkItem(context.Background(), "TAS-1", 45, time.UnixMilli(1700000000000), "note") if err != nil { t.Fatal(err) } if wi.ID != "wi-2" { t.Fatalf("wi = %+v", wi) } dur := created["duration"].(map[string]any) if int(dur["minutes"].(float64)) != 45 { t.Fatalf("created = %+v", created) } } func TestApplyCommand(t *testing.T) { var got string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/commands" { t.Fatalf("path %s", r.URL.Path) } body, _ := io.ReadAll(r.Body) var payload map[string]any _ = json.Unmarshal(body, &payload) got = fmt.Sprint(payload["query"]) w.WriteHeader(http.StatusOK) })) defer srv.Close() c := &youtrack.Client{BaseURL: srv.URL, Token: "tok", HTTPClient: srv.Client()} if err := c.ApplyCommand(context.Background(), "TAS-1", "Status Open"); err != nil { t.Fatal(err) } if got != "Status Open" { t.Fatalf("query = %q", got) } }