feat(incident): add incident subscribe command

This commit is contained in:
Vitali Tatarintev 2023-06-15 10:24:31 +00:00 committed by Shekhar Patnaik
parent 954b4a2cdb
commit da7e47aa8e
11 changed files with 362 additions and 136 deletions

View File

@ -2,5 +2,8 @@ package api
import "errors"
// ErrIssuableUserNotSubscribed received when trying to unsubscribe from issue the user not subscribed to
// ErrIssuableUserNotSubscribed received when trying to unsubscribe from an issue the user is not subscribed to
var ErrIssuableUserNotSubscribed = errors.New("you are not subscribed to this issue")
// ErrIssuableUserAlreadySubscribed received when trying to subscribe to an issue the user is already subscribed to
var ErrIssuableUserAlreadySubscribed = errors.New("you are already subscribed to this issue")

View File

@ -2,7 +2,6 @@
package api
import (
"errors"
"net/http"
"github.com/xanzy/go-gitlab"
@ -153,7 +152,7 @@ var SubscribeToIssue = func(client *gitlab.Client, projectID interface{}, issueI
if resp != nil {
// If the user is already subscribed to the issue, the status code 304 is returned.
if resp.StatusCode == http.StatusNotModified {
return nil, errors.New("you are already subscribed to this issue")
return nil, ErrIssuableUserAlreadySubscribed
}
}
return issue, err

View File

@ -7,6 +7,7 @@ import (
incidentCloseCmd "gitlab.com/gitlab-org/cli/commands/incident/close"
incidentListCmd "gitlab.com/gitlab-org/cli/commands/incident/list"
incidentReopenCmd "gitlab.com/gitlab-org/cli/commands/incident/reopen"
incidentSubscribeCmd "gitlab.com/gitlab-org/cli/commands/incident/subscribe"
incidentUnsubscribeCmd "gitlab.com/gitlab-org/cli/commands/incident/unsubscribe"
incidentViewCmd "gitlab.com/gitlab-org/cli/commands/incident/view"
@ -36,6 +37,7 @@ func NewCmdIncident(f *cmdutils.Factory) *cobra.Command {
incidentCmd.AddCommand(incidentViewCmd.NewCmdView(f))
incidentCmd.AddCommand(incidentCloseCmd.NewCmdClose(f))
incidentCmd.AddCommand(incidentReopenCmd.NewCmdReopen(f))
incidentCmd.AddCommand(incidentSubscribeCmd.NewCmdSubscribe(f))
incidentCmd.AddCommand(incidentUnsubscribeCmd.NewCmdUnsubscribe(f))
return incidentCmd
}

View File

@ -0,0 +1,13 @@
package subscribe
import (
"github.com/spf13/cobra"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
issuableSubscribeCmd "gitlab.com/gitlab-org/cli/commands/issuable/subscribe"
)
func NewCmdSubscribe(f *cmdutils.Factory) *cobra.Command {
return issuableSubscribeCmd.NewCmdSubscribe(f, issuable.TypeIncident)
}

View File

@ -0,0 +1,89 @@
package subscribe
import (
"errors"
"fmt"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
"gitlab.com/gitlab-org/cli/commands/issue/issueutils"
)
var subscribingMessage = map[issuable.IssueType]string{
issuable.TypeIssue: "Subscribing to Issue",
issuable.TypeIncident: "Subscribing to Incident",
}
func NewCmdSubscribe(f *cmdutils.Factory, issueType issuable.IssueType) *cobra.Command {
examplePath := "issues/123"
if issueType == issuable.TypeIncident {
examplePath = "issues/incident/123"
}
issueSubscribeCmd := &cobra.Command{
Use: "subscribe <id>",
Short: fmt.Sprintf(`Subscribe to an %s`, issueType),
Long: ``,
Aliases: []string{"sub"},
Example: heredoc.Doc(fmt.Sprintf(`
glab %[1]s subscribe 123
glab %[1]s sub 123
glab %[1]s subscribe https://gitlab.com/OWNER/REPO/-/%[2]s
`, issueType, examplePath)),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c := f.IO.Color()
apiClient, err := f.HttpClient()
if err != nil {
return err
}
issues, repo, err := issueutils.IssuesFromArgs(apiClient, f.BaseRepo, args)
if err != nil {
return err
}
for _, issue := range issues {
valid, msg := issuable.ValidateIncidentCmd(issueType, "subscribe", issue)
if !valid {
fmt.Fprintln(f.IO.StdOut, msg)
continue
}
if f.IO.IsaTTY && f.IO.IsErrTTY {
fmt.Fprintf(
f.IO.StdOut,
"- %s #%d in %s\n",
subscribingMessage[issueType],
issue.IID,
c.Cyan(repo.FullName()),
)
}
issue, err := api.SubscribeToIssue(apiClient, repo.FullName(), issue.IID, nil)
if err != nil {
if errors.Is(err, api.ErrIssuableUserAlreadySubscribed) {
fmt.Fprintf(
f.IO.StdOut,
"%s You are already subscribed to this %s\n\n",
c.FailedIcon(),
issueType,
)
return nil // the error already handled
}
return err
}
fmt.Fprintln(f.IO.StdOut, c.GreenCheck(), "Subscribed")
fmt.Fprintln(f.IO.StdOut, issueutils.DisplayIssue(c, issue, f.IO.IsaTTY))
}
return nil
},
}
return issueSubscribeCmd
}

View File

@ -0,0 +1,207 @@
package subscribe
import (
"fmt"
"net/http"
"testing"
"github.com/MakeNowJust/heredoc"
"github.com/google/shlex"
"github.com/stretchr/testify/require"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
"gitlab.com/gitlab-org/cli/internal/glrepo"
"gitlab.com/gitlab-org/cli/pkg/httpmock"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"gitlab.com/gitlab-org/cli/test"
)
func runCommand(rt http.RoundTripper, issuableID string, issueType issuable.IssueType) (*test.CmdOut, error) {
ios, _, stdout, stderr := iostreams.Test()
ios.IsaTTY = true
ios.IsErrTTY = true
factory := &cmdutils.Factory{
IO: ios,
HttpClient: func() (*gitlab.Client, error) {
a, err := api.TestClient(&http.Client{Transport: rt}, "", "", false)
if err != nil {
return nil, err
}
return a.Lab(), err
},
BaseRepo: func() (glrepo.Interface, error) {
return glrepo.New("OWNER", "REPO"), nil
},
}
_, _ = factory.HttpClient()
cmd := NewCmdSubscribe(factory, issueType)
argv, err := shlex.Split(issuableID)
if err != nil {
return nil, err
}
cmd.SetArgs(argv)
_, err = cmd.ExecuteC()
return &test.CmdOut{
OutBuf: stdout,
ErrBuf: stderr,
}, err
}
func mockIssuableGet(fakeHTTP *httpmock.Mocker, id int, issueType string, subscribed bool) {
fakeHTTP.RegisterResponder(http.MethodGet, fmt.Sprintf("/projects/OWNER/REPO/issues/%d", id),
httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf(`{
"id": %d,
"iid": %d,
"title": "test issue",
"subscribed": %t,
"issue_type": "%s",
"created_at": "2023-05-02T10:51:26.371Z"
}`, id, id, subscribed, issueType)),
)
}
func mockIssuableSubscribe(fakeHTTP *httpmock.Mocker, id int, issueType string, subscribed bool) {
fakeHTTP.RegisterResponder(http.MethodPost, fmt.Sprintf("/projects/OWNER/REPO/issues/%d/subscribe", id),
func(req *http.Request) (*http.Response, error) {
resp, _ := httpmock.NewStringResponse(http.StatusOK, fmt.Sprintf(`{
"id": %d,
"iid": %d,
"subscribed": %t,
"issue_type": "%s",
"created_at": "2023-05-02T10:51:26.371Z"
}`, id, id, subscribed, issueType))(req)
return resp, nil
},
)
}
func TestIssuableSubscribe(t *testing.T) {
t.Run("issue_subscribe", func(t *testing.T) {
iid := 1
fakeHTTP := httpmock.New()
mockIssuableGet(fakeHTTP, iid, string(issuable.TypeIssue), false)
mockIssuableSubscribe(fakeHTTP, iid, string(issuable.TypeIssue), true)
output, err := runCommand(fakeHTTP, fmt.Sprint(iid), issuable.TypeIssue)
wantOutput := heredoc.Doc(`
- Subscribing to Issue #1 in OWNER/REPO
Subscribed
`)
require.NoErrorf(t, err, "error running command `issue subscribe %d`", iid)
require.Contains(t, output.String(), wantOutput)
require.Empty(t, output.Stderr())
})
t.Run("incident_subscribe", func(t *testing.T) {
iid := 2
fakeHTTP := httpmock.New()
mockIssuableGet(fakeHTTP, iid, string(issuable.TypeIncident), false)
mockIssuableSubscribe(fakeHTTP, iid, string(issuable.TypeIncident), true)
output, err := runCommand(fakeHTTP, fmt.Sprint(iid), issuable.TypeIncident)
wantOutput := heredoc.Doc(`
- Subscribing to Incident #2 in OWNER/REPO
Subscribed
`)
require.NoErrorf(t, err, "error running command `incident subscribe %d`", iid)
require.Contains(t, output.String(), wantOutput)
require.Empty(t, output.Stderr())
})
t.Run("incident_subscribe_using_issue_command", func(t *testing.T) {
iid := 2
fakeHTTP := httpmock.New()
mockIssuableGet(fakeHTTP, iid, string(issuable.TypeIncident), false)
mockIssuableSubscribe(fakeHTTP, iid, string(issuable.TypeIncident), true)
output, err := runCommand(fakeHTTP, fmt.Sprint(iid), issuable.TypeIssue)
wantOutput := heredoc.Doc(`
- Subscribing to Issue #2 in OWNER/REPO
Subscribed
`)
require.NoErrorf(t, err, "error running command `issue subscribe %d`", iid)
require.Contains(t, output.String(), wantOutput)
require.Empty(t, output.Stderr())
})
t.Run("issue_subscribe_using_incident_command", func(t *testing.T) {
iid := 1
fakeHTTP := httpmock.New()
mockIssuableGet(fakeHTTP, iid, string(issuable.TypeIssue), false)
mockIssuableSubscribe(fakeHTTP, iid, string(issuable.TypeIssue), true)
output, err := runCommand(fakeHTTP, fmt.Sprint(iid), issuable.TypeIncident)
wantOutput := "Incident not found, but an issue with the provided ID exists. Run `glab issue subscribe <id>` to subscribe.\n"
require.NoErrorf(t, err, "error running command `incident subscribe %d`", iid)
require.Contains(t, output.String(), wantOutput)
require.Empty(t, output.Stderr())
})
t.Run("issue_subscribe_to_already_subscribed_issue", func(t *testing.T) {
iid := 3
fakeHTTP := httpmock.New()
mockIssuableGet(fakeHTTP, iid, string(issuable.TypeIssue), true)
fakeHTTP.RegisterResponder(http.MethodPost, "/projects/OWNER/REPO/issues/3/subscribe",
httpmock.NewStringResponse(http.StatusNotModified, ``),
)
output, err := runCommand(fakeHTTP, fmt.Sprint(iid), issuable.TypeIssue)
wantOutput := heredoc.Doc(`
- Subscribing to Issue #3 in OWNER/REPO
x You are already subscribed to this issue
`)
require.NoErrorf(t, err, "error running command `issue subscribe %d`", iid)
require.Contains(t, output.String(), wantOutput)
require.Empty(t, output.Stderr())
})
t.Run("incident_subscribe_to_already_subscribed_incident", func(t *testing.T) {
iid := 3
fakeHTTP := httpmock.New()
mockIssuableGet(fakeHTTP, iid, string(issuable.TypeIncident), true)
fakeHTTP.RegisterResponder(http.MethodPost, "/projects/OWNER/REPO/issues/3/subscribe",
httpmock.NewStringResponse(http.StatusNotModified, ``),
)
output, err := runCommand(fakeHTTP, fmt.Sprint(iid), issuable.TypeIncident)
wantOutput := heredoc.Doc(`
- Subscribing to Incident #3 in OWNER/REPO
x You are already subscribed to this incident
`)
require.NoErrorf(t, err, "error running command `incident subscribe %d`", iid)
require.Contains(t, output.String(), wantOutput)
require.Empty(t, output.Stderr())
})
t.Run("issue_not_found", func(t *testing.T) {
fakeHTTP := httpmock.New()
fakeHTTP.RegisterResponder(http.MethodGet, "/projects/OWNER/REPO/issues/404",
httpmock.NewStringResponse(http.StatusNotFound, `{"message": "404 not found"}`),
)
iid := 404
_, err := runCommand(fakeHTTP, fmt.Sprint(iid), issuable.TypeIssue)
require.Contains(t, err.Error(), "404 not found")
})
}

View File

@ -1,55 +1,13 @@
package subscribe
import (
"fmt"
"github.com/MakeNowJust/heredoc"
"github.com/spf13/cobra"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdutils"
"gitlab.com/gitlab-org/cli/commands/issue/issueutils"
"gitlab.com/gitlab-org/cli/commands/issuable"
issuableSubscribeCmd "gitlab.com/gitlab-org/cli/commands/issuable/subscribe"
)
func NewCmdSubscribe(f *cmdutils.Factory) *cobra.Command {
issueSubscribeCmd := &cobra.Command{
Use: "subscribe <id>",
Short: `Subscribe to an issue`,
Long: ``,
Aliases: []string{"sub"},
Example: heredoc.Doc(`
glab issue subscribe 123
glab issue sub 123
glab issue subscribe https://gitlab.com/profclems/glab/-/issues/123
`),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c := f.IO.Color()
apiClient, err := f.HttpClient()
if err != nil {
return err
}
issues, repo, err := issueutils.IssuesFromArgs(apiClient, f.BaseRepo, args)
if err != nil {
return err
}
for _, issue := range issues {
if f.IO.IsErrTTY && f.IO.IsaTTY {
fmt.Fprintf(f.IO.StdErr, "- Subscribing to Issue #%d in %s\n", issue.IID, c.Cyan(repo.FullName()))
}
issue, err := api.SubscribeToIssue(apiClient, repo.FullName(), issue.IID, nil)
if err != nil {
return err
}
fmt.Fprintln(f.IO.StdErr, c.GreenCheck(), "Subscribed")
fmt.Fprintln(f.IO.StdOut, issueutils.DisplayIssue(c, issue, f.IO.IsaTTY))
}
return nil
},
}
return issueSubscribeCmd
return issuableSubscribeCmd.NewCmdSubscribe(f, issuable.TypeIssue)
}

View File

@ -1,86 +0,0 @@
package subscribe
import (
"fmt"
"testing"
"time"
"gitlab.com/gitlab-org/cli/test"
"gitlab.com/gitlab-org/cli/pkg/iostreams"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/xanzy/go-gitlab"
"gitlab.com/gitlab-org/cli/api"
"gitlab.com/gitlab-org/cli/commands/cmdtest"
)
func TestNewCmdSubscribe_Integration(t *testing.T) {
glTestHost := test.GetHostOrSkip(t)
t.Parallel()
oldSubscribeIssue := api.SubscribeToIssue
timer, _ := time.Parse(time.RFC3339, "2014-11-12T11:45:26.371Z")
api.SubscribeToIssue = func(client *gitlab.Client, projectID interface{}, issueID int, opts gitlab.RequestOptionFunc) (*gitlab.Issue, error) {
if projectID == "" || projectID == "WRONG_REPO" || projectID == "expected_err" || issueID == 0 {
return nil, fmt.Errorf("error expected")
}
return &gitlab.Issue{
ID: issueID,
IID: issueID,
State: "closed",
Description: "Dummy description for issue " + string(rune(issueID)),
Author: &gitlab.IssueAuthor{
ID: 1,
Name: "John Dev Wick",
Username: "jdwick",
},
CreatedAt: &timer,
}, nil
}
testCases := []struct {
Name string
Issue string
stderr string
wantErr bool
}{
{
Name: "Issue Exists",
Issue: "1",
stderr: "- Subscribing to Issue #1 in cli-automated-testing/test\n✓ Subscribed\n",
},
{
Name: "Issue Does Not Exist",
Issue: "0",
stderr: "- Subscribing to Issue #0 in cli-automated-testing/test\nerror expected\n",
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
io, _, _, stderr := iostreams.Test()
f := cmdtest.StubFactory(glTestHost + "/cli-automated-testing/test")
f.IO = io
f.IO.IsaTTY = true
f.IO.IsErrTTY = true
cmd := NewCmdSubscribe(f)
cmd.Flags().StringP("repo", "R", "", "")
_, err := cmdtest.RunCommand(cmd, tc.Issue)
if tc.wantErr {
require.Error(t, err)
return
} else {
require.NoError(t, err)
}
assert.Equal(t, tc.stderr, stderr.String())
})
}
api.SubscribeToIssue = oldSubscribeIssue
}

View File

@ -37,5 +37,6 @@ glab incident list
- [close](close.md)
- [list](list.md)
- [reopen](reopen.md)
- [subscribe](subscribe.md)
- [unsubscribe](unsubscribe.md)
- [view](view.md)

View File

@ -0,0 +1,40 @@
---
stage: Create
group: Code Review
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
<!--
This documentation is auto generated by a script.
Please do not edit this file directly. Run `make gen-docs` instead.
-->
# `glab incident subscribe`
Subscribe to an incident
```plaintext
glab incident subscribe <id> [flags]
```
## Aliases
```plaintext
sub
```
## Examples
```plaintext
glab incident subscribe 123
glab incident sub 123
glab incident subscribe https://gitlab.com/OWNER/REPO/-/issues/incident/123
```
## Options inherited from parent commands
```plaintext
--help Show help for command
-R, --repo OWNER/REPO Select another repository using the OWNER/REPO or `GROUP/NAMESPACE/REPO` format or full URL or git URL
```

View File

@ -28,7 +28,7 @@ sub
```plaintext
glab issue subscribe 123
glab issue sub 123
glab issue subscribe https://gitlab.com/profclems/glab/-/issues/123
glab issue subscribe https://gitlab.com/OWNER/REPO/-/issues/123
```