package itemedit import ( "testing" "github.com/cli/cli/v2/pkg/cmd/project/shared/queries" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/google/shlex" "github.com/stretchr/testify/assert" "gopkg.in/h2non/gock.v1" ) func TestNewCmdeditItem(t *testing.T) { tests := []struct { name string cli string wants editItemOpts wantsErr bool wantsErrMsg string wantsExporter bool }{ { name: "missing-id", cli: "", wantsErr: true, wantsErrMsg: "required flag(s) \"id\" not set", }, { name: "invalid-flags", cli: "--id 123 --text t --date 2023-01-01", wantsErr: true, wantsErrMsg: "only one of `--text`, `--number`, `--date`, `--single-select-option-id` or `--iteration-id` may be used", }, { name: "item-id", cli: "--id 123", wants: editItemOpts{ itemID: "123", }, }, { name: "number", cli: "--number 456 --id 123", wants: editItemOpts{ number: 456, itemID: "123", }, }, { name: "number with floating point value", cli: "--number 123.45 --id 123", wants: editItemOpts{ number: 123.45, itemID: "123", }, }, { name: "number zero", cli: "--number 0 --id 123", wants: editItemOpts{ number: 0, itemID: "123", }, }, { name: "field-id", cli: "--field-id FIELD_ID --id 123", wants: editItemOpts{ fieldID: "FIELD_ID", itemID: "123", }, }, { name: "project-id", cli: "--project-id PROJECT_ID --id 123", wants: editItemOpts{ projectID: "PROJECT_ID", itemID: "123", }, }, { name: "text", cli: "--text t --id 123", wants: editItemOpts{ text: "t", itemID: "123", }, }, { name: "date", cli: "--date 2023-01-01 --id 123", wants: editItemOpts{ date: "2023-01-01", itemID: "123", }, }, { name: "single-select-option-id", cli: "--single-select-option-id OPTION_ID --id 123", wants: editItemOpts{ singleSelectOptionID: "OPTION_ID", itemID: "123", }, }, { name: "iteration-id", cli: "--iteration-id ITERATION_ID --id 123", wants: editItemOpts{ iterationID: "ITERATION_ID", itemID: "123", }, }, { name: "clear", cli: "--id 123 --field-id FIELD_ID --project-id PROJECT_ID --clear", wants: editItemOpts{ itemID: "123", fieldID: "FIELD_ID", projectID: "PROJECT_ID", clear: true, }, }, { name: "json", cli: "--format json --id 123", wants: editItemOpts{ itemID: "123", }, wantsExporter: true, }, { name: "draft issue body only", cli: "--id 123 --body foobar", wants: editItemOpts{ itemID: "123", body: "foobar", bodyChanged: true, }, }, } t.Setenv("GH_TOKEN", "auth-token") for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, _, _ := iostreams.Test() f := &cmdutil.Factory{ IOStreams: ios, } argv, err := shlex.Split(tt.cli) assert.NoError(t, err) var gotOpts editItemOpts cmd := NewCmdEditItem(f, func(config editItemConfig) error { gotOpts = config.opts return nil }) cmd.SetArgs(argv) _, err = cmd.ExecuteC() if tt.wantsErr { assert.Error(t, err) assert.Equal(t, tt.wantsErrMsg, err.Error()) return } assert.NoError(t, err) assert.Equal(t, tt.wants.number, gotOpts.number) assert.Equal(t, tt.wants.itemID, gotOpts.itemID) assert.Equal(t, tt.wantsExporter, gotOpts.exporter != nil) assert.Equal(t, tt.wants.title, gotOpts.title) assert.Equal(t, tt.wants.fieldID, gotOpts.fieldID) assert.Equal(t, tt.wants.projectID, gotOpts.projectID) assert.Equal(t, tt.wants.text, gotOpts.text) assert.Equal(t, tt.wants.number, gotOpts.number) assert.Equal(t, tt.wants.date, gotOpts.date) assert.Equal(t, tt.wants.singleSelectOptionID, gotOpts.singleSelectOptionID) assert.Equal(t, tt.wants.iterationID, gotOpts.iterationID) assert.Equal(t, tt.wants.clear, gotOpts.clear) assert.Equal(t, tt.wants.titleChanged, gotOpts.titleChanged) assert.Equal(t, tt.wants.bodyChanged, gotOpts.bodyChanged) assert.Equal(t, tt.wants.body, gotOpts.body) }) } } func TestRunItemEdit_Draft(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) // edit item gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation EditDraftIssueItem.*","variables":{"input":{"draftIssueId":"DI_item_id","title":"a title","body":"a new body"}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2DraftIssue": map[string]interface{}{ "draftIssue": map[string]interface{}{ "title": "a title", "body": "a new body", }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() ios.SetStdoutTTY(true) config := editItemConfig{ io: ios, opts: editItemOpts{ title: "a title", titleChanged: true, body: "a new body", bodyChanged: true, itemID: "DI_item_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal( t, "Edited draft issue \"a title\"\n", stdout.String()) } func TestRunItemEdit_DraftTitleOnly(t *testing.T) { defer gock.Off() gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"query DraftIssueByID.*","variables":{"id":"DI_item_id"}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "node": map[string]interface{}{ "id": "DI_item_id", "title": "existing title", "body": "existing body", }, }, }) gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation EditDraftIssueItem.*","variables":{"input":{"draftIssueId":"DI_item_id","title":"new title","body":"existing body"}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2DraftIssue": map[string]interface{}{ "draftIssue": map[string]interface{}{ "title": "new title", "body": "existing body", }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() ios.SetStdoutTTY(true) config := editItemConfig{ io: ios, opts: editItemOpts{ title: "new title", titleChanged: true, bodyChanged: false, itemID: "DI_item_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal( t, "Edited draft issue \"new title\"\n", stdout.String()) } func TestRunItemEdit_DraftBodyOnly(t *testing.T) { defer gock.Off() gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"query DraftIssueByID.*","variables":{"id":"DI_item_id"}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "node": map[string]interface{}{ "id": "DI_item_id", "title": "existing title", "body": "existing body", }, }, }) gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation EditDraftIssueItem.*","variables":{"input":{"draftIssueId":"DI_item_id","title":"existing title","body":"new body"}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2DraftIssue": map[string]interface{}{ "draftIssue": map[string]interface{}{ "title": "existing title", "body": "new body", }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() ios.SetStdoutTTY(true) config := editItemConfig{ io: ios, opts: editItemOpts{ titleChanged: false, body: "new body", bodyChanged: true, itemID: "DI_item_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal( t, "Edited draft issue \"existing title\"\n", stdout.String()) } func TestRunItemEdit_DraftFetchError(t *testing.T) { defer gock.Off() gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"query DraftIssueByID.*","variables":{"id":"DI_item_id"}}`). Reply(200). JSON(map[string]interface{}{ "errors": []map[string]interface{}{ { "type": "NOT_FOUND", "message": "Could not resolve to a node with the global id of 'DI_item_id' (node)", }, }, }) client := queries.NewTestClient() ios, _, _, _ := iostreams.Test() config := editItemConfig{ io: ios, opts: editItemOpts{ title: "new title", titleChanged: true, bodyChanged: false, itemID: "DI_item_id", }, client: client, } err := runEditItem(config) assert.Error(t, err) assert.Contains(t, err.Error(), "Could not resolve to a node") } func TestRunItemEdit_Text(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) // edit item gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"text":"item text"}}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2ItemFieldValue": map[string]interface{}{ "projectV2Item": map[string]interface{}{ "ID": "item_id", "content": map[string]interface{}{ "body": "body", "title": "title", "number": 1, "repository": map[string]interface{}{ "nameWithOwner": "my-repo", }, }, }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() config := editItemConfig{ io: ios, opts: editItemOpts{ text: "item text", itemID: "item_id", projectID: "project_id", fieldID: "field_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) } func TestRunItemEdit_Number(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) // edit item gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"number":123.45}}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2ItemFieldValue": map[string]interface{}{ "projectV2Item": map[string]interface{}{ "ID": "item_id", "content": map[string]interface{}{ "__typename": "Issue", "body": "body", "title": "title", "number": 1, "repository": map[string]interface{}{ "nameWithOwner": "my-repo", }, }, }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() ios.SetStdoutTTY(true) config := editItemConfig{ io: ios, opts: editItemOpts{ number: 123.45, numberChanged: true, itemID: "item_id", projectID: "project_id", fieldID: "field_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal( t, "Edited item \"title\"\n", stdout.String()) } func TestRunItemEdit_NumberZero(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) // edit item gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"number":0}}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2ItemFieldValue": map[string]interface{}{ "projectV2Item": map[string]interface{}{ "ID": "item_id", "content": map[string]interface{}{ "__typename": "Issue", "body": "body", "title": "title", "number": 1, "repository": map[string]interface{}{ "nameWithOwner": "my-repo", }, }, }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() ios.SetStdoutTTY(true) config := editItemConfig{ io: ios, opts: editItemOpts{ number: 0, numberChanged: true, itemID: "item_id", projectID: "project_id", fieldID: "field_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal( t, "Edited item \"title\"\n", stdout.String()) } func TestRunItemEdit_Date(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) // edit item gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"date":"2023-01-01T00:00:00Z"}}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2ItemFieldValue": map[string]interface{}{ "projectV2Item": map[string]interface{}{ "ID": "item_id", "content": map[string]interface{}{ "__typename": "Issue", "body": "body", "title": "title", "number": 1, "repository": map[string]interface{}{ "nameWithOwner": "my-repo", }, }, }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() config := editItemConfig{ io: ios, opts: editItemOpts{ date: "2023-01-01", itemID: "item_id", projectID: "project_id", fieldID: "field_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) } func TestRunItemEdit_SingleSelect(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) // edit item gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"singleSelectOptionId":"option_id"}}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2ItemFieldValue": map[string]interface{}{ "projectV2Item": map[string]interface{}{ "ID": "item_id", "content": map[string]interface{}{ "__typename": "Issue", "body": "body", "title": "title", "number": 1, "repository": map[string]interface{}{ "nameWithOwner": "my-repo", }, }, }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() config := editItemConfig{ io: ios, opts: editItemOpts{ singleSelectOptionID: "option_id", itemID: "item_id", projectID: "project_id", fieldID: "field_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal(t, "", stdout.String()) } func TestRunItemEdit_Iteration(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) // edit item gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation UpdateItemValues.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id","value":{"iterationId":"option_id"}}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2ItemFieldValue": map[string]interface{}{ "projectV2Item": map[string]interface{}{ "ID": "item_id", "content": map[string]interface{}{ "__typename": "Issue", "body": "body", "title": "title", "number": 1, "repository": map[string]interface{}{ "nameWithOwner": "my-repo", }, }, }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() ios.SetStdoutTTY(true) config := editItemConfig{ io: ios, opts: editItemOpts{ iterationID: "option_id", itemID: "item_id", projectID: "project_id", fieldID: "field_id", }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal( t, "Edited item \"title\"\n", stdout.String()) } func TestRunItemEdit_NoChanges(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) client := queries.NewTestClient() ios, _, stdout, stderr := iostreams.Test() ios.SetStdoutTTY(true) config := editItemConfig{ io: ios, opts: editItemOpts{}, client: client, } err := runEditItem(config) assert.Error(t, err, "SilentError") assert.Equal(t, "", stdout.String()) assert.Equal(t, "error: no changes to make\n", stderr.String()) } func TestRunItemEdit_InvalidID(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) client := queries.NewTestClient() config := editItemConfig{ opts: editItemOpts{ title: "a title", titleChanged: true, body: "a new body", bodyChanged: true, itemID: "item_id", }, client: client, } err := runEditItem(config) assert.Error(t, err, "ID must be the ID of the draft issue content which is prefixed with `DI_`") } func TestRunItemEdit_Clear(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation ClearItemFieldValue.*","variables":{"input":{"projectId":"project_id","itemId":"item_id","fieldId":"field_id"}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "clearProjectV2ItemFieldValue": map[string]interface{}{ "projectV2Item": map[string]interface{}{ "ID": "item_id", "content": map[string]interface{}{ "__typename": "Issue", "body": "body", "title": "title", "number": 1, "repository": map[string]interface{}{ "nameWithOwner": "my-repo", }, }, }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() ios.SetStdoutTTY(true) config := editItemConfig{ io: ios, opts: editItemOpts{ itemID: "item_id", projectID: "project_id", fieldID: "field_id", clear: true, }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.Equal(t, "Edited item \"title\"\n", stdout.String()) } func TestRunItemEdit_JSON(t *testing.T) { defer gock.Off() // gock.Observe(gock.DumpRequest) // edit item gock.New("https://api.github.com"). Post("/graphql"). BodyString(`{"query":"mutation EditDraftIssueItem.*","variables":{"input":{"draftIssueId":"DI_item_id","title":"a title","body":"a new body"}}}`). Reply(200). JSON(map[string]interface{}{ "data": map[string]interface{}{ "updateProjectV2DraftIssue": map[string]interface{}{ "draftIssue": map[string]interface{}{ "id": "DI_item_id", "title": "a title", "body": "a new body", }, }, }, }) client := queries.NewTestClient() ios, _, stdout, _ := iostreams.Test() config := editItemConfig{ io: ios, opts: editItemOpts{ title: "a title", titleChanged: true, body: "a new body", bodyChanged: true, itemID: "DI_item_id", exporter: cmdutil.NewJSONExporter(), }, client: client, } err := runEditItem(config) assert.NoError(t, err) assert.JSONEq( t, `{"id":"DI_item_id","title":"a title","body":"a new body","type":"DraftIssue"}`, stdout.String()) }