Repository: ctreminiom/go-atlassian
Pull request #197 (2023-05-13 17:12:05)

:sparkles: Added the ability to edit the board backlog


Patch:
diff --git a/jira/agile/api_client_impl.go b/jira/agile/api_client_impl.go
index f151b11..ee661ff 100644
--- a/jira/agile/api_client_impl.go
+++ b/jira/agile/api_client_impl.go
@@ -52,18 +52,20 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 	client.Board = boardService
 	client.Epic = epicService
 	client.Sprint = sprintService
+	client.Backlog = internal.NewBoardBacklogService(client, "1.0")
 	client.Auth = internal.NewAuthenticationService(client)
 
 	return client, nil
 }
 
 type Client struct {
-	HTTP   common.HttpClient
-	Site   *url.URL
-	Auth   common.Authentication
-	Board  *internal.BoardService
-	Epic   *internal.EpicService
-	Sprint *internal.SprintService
+	HTTP    common.HttpClient
+	Site    *url.URL
+	Auth    common.Authentication
+	Board   *internal.BoardService
+	Backlog *internal.BoardBacklogService
+	Epic    *internal.EpicService
+	Sprint  *internal.SprintService
 }
 
 func (c *Client) NewFormRequest(ctx context.Context, method, apiEndpoint, contentType string, payload io.Reader) (*http.Request, error) {
diff --git a/jira/agile/internal/board_backlog_impl.go b/jira/agile/internal/board_backlog_impl.go
new file mode 100644
index 0000000..d80806f
--- /dev/null
+++ b/jira/agile/internal/board_backlog_impl.go
@@ -0,0 +1,96 @@
+package internal
+
+import (
+	"context"
+	"fmt"
+	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/agile"
+	"net/http"
+)
+
+func NewBoardBacklogService(client service.Client, version string) *BoardBacklogService {
+
+	return &BoardBacklogService{
+		internalClient: &internalBoardBacklogImpl{c: client, version: version},
+	}
+}
+
+type BoardBacklogService struct {
+	internalClient agile.BoardBacklogConnector
+}
+
+// Move moves issues to the backlog.
+//
+// This operation is equivalent to remove future and active sprints from a given set of issues.
+//
+// At most 50 issues may be moved at once.
+//
+// POST /rest/agile/1.0/backlog/issue
+//
+// https://docs.go-atlassian.io/jira-agile/boards/backlog#move-issues-to-backlog
+func (b *BoardBacklogService) Move(ctx context.Context, issues []string) (*model.ResponseScheme, error) {
+	return b.internalClient.Move(ctx, issues)
+}
+
+// MoveTo moves issues to the backlog of a particular board (if they are already on that board).
+//
+// This operation is equivalent to remove future and active sprints from a given set of issues if the board has sprints.
+//
+// If the board does not have sprints this will put the issues back into the backlog from the board.
+//
+// At most 50 issues may be moved at once.
+//
+// POST /rest/agile/1.0/backlog/{boardId}/issue
+//
+// https://docs.go-atlassian.io/jira-agile/boards/backlog#move-issues-to-backlog
+func (b *BoardBacklogService) MoveTo(ctx context.Context, boardID int, payload *model.BoardBacklogPayloadScheme) (*model.ResponseScheme, error) {
+	return b.internalClient.MoveTo(ctx, boardID, payload)
+}
+
+type internalBoardBacklogImpl struct {
+	c       service.Client
+	version string
+}
+
+func (i *internalBoardBacklogImpl) Move(ctx context.Context, issues []string) (*model.ResponseScheme, error) {
+
+	payload := struct {
+		Issues []string `json:"issues"`
+	}{Issues: issues}
+
+	reader, err := i.c.TransformStructToReader(&payload)
+	if err != nil {
+		return nil, err
+	}
+
+	endpoint := fmt.Sprintf("rest/agile/%v/backlog/issue", i.version)
+
+	request, err := i.c.NewRequest(ctx, http.MethodPost, endpoint, reader)
+	if err != nil {
+		return nil, err
+	}
+
+	return i.c.Call(request, nil)
+}
+
+func (i *internalBoardBacklogImpl) MoveTo(ctx context.Context, boardID int, payload *model.BoardBacklogPayloadScheme) (*model.ResponseScheme, error) {
+
+	if boardID == 0 {
+		return nil, model.ErrNoBoardIDError
+	}
+
+	reader, err := i.c.TransformStructToReader(payload)
+	if err != nil {
+		return nil, err
+	}
+
+	endpoint := fmt.Sprintf("rest/agile/%v/backlog/%v/issue", i.version, boardID)
+
+	request, err := i.c.NewRequest(ctx, http.MethodPost, endpoint, reader)
+	if err != nil {
+		return nil, err
+	}
+
+	return i.c.Call(request, nil)
+}
diff --git a/jira/agile/internal/board_impl.go b/jira/agile/internal/board_impl.go
index 0b5e6ea..e23d808 100644
--- a/jira/agile/internal/board_impl.go
+++ b/jira/agile/internal/board_impl.go
@@ -30,7 +30,6 @@ type BoardService struct {
 // Get returns the board for the given board ID.
 // This board will only be returned if the user has permission to view it.
 //
-//
 // Admins without the view permission will see the board as a private one,
 //
 // so will see only a subset of the board's data (board location for instance).
@@ -53,7 +52,7 @@ func (b *BoardService) Create(ctx context.Context, payload *model.BoardPayloadSc
 
 // Filter returns any boards which use the provided filter id.
 //
-// This method can be executed by users without a valid software license in order
+// # This method can be executed by users without a valid software license in order
 //
 // to find which boards are using a particular filter.
 //
diff --git a/pkg/infra/models/agile_board.go b/pkg/infra/models/agile_board.go
index 8ca586f..1746c6a 100644
--- a/pkg/infra/models/agile_board.go
+++ b/pkg/infra/models/agile_board.go
@@ -208,3 +208,10 @@ type IssueOptionScheme struct {
 	Fields        []string
 	Expand        []string
 }
+
+type BoardBacklogPayloadScheme struct {
+	Issues            []string `json:"issues,omitempty"`
+	RankBeforeIssue   string   `json:"rankBeforeIssue,omitempty"`
+	RankAfterIssue    string   `json:"rankAfterIssue,omitempty"`
+	RankCustomFieldId int      `json:"rankCustomFieldId,omitempty"`
+}
diff --git a/service/agile/backlog.go b/service/agile/backlog.go
new file mode 100644
index 0000000..07b1c1a
--- /dev/null
+++ b/service/agile/backlog.go
@@ -0,0 +1,35 @@
+package agile
+
+import (
+	"context"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+)
+
+// BoardBacklogConnector represents the board backlogs.
+// Use it to search, get, create, delete, and change backlogs.
+type BoardBacklogConnector interface {
+
+	// Move moves issues to the backlog.
+	//
+	// This operation is equivalent to remove future and active sprints from a given set of issues.
+	//
+	// At most 50 issues may be moved at once.
+	//
+	// POST /rest/agile/1.0/backlog/issue
+	//
+	// https://docs.go-atlassian.io/jira-agile/boards/backlog#move-issues-to-backlog
+	Move(ctx context.Context, issues []string) (*models.ResponseScheme, error)
+
+	// MoveTo moves issues to the backlog of a particular board (if they are already on that board).
+	//
+	// This operation is equivalent to remove future and active sprints from a given set of issues if the board has sprints.
+	//
+	// If the board does not have sprints this will put the issues back into the backlog from the board.
+	//
+	// At most 50 issues may be moved at once.
+	//
+	// POST /rest/agile/1.0/backlog/{boardId}/issue
+	//
+	// https://docs.go-atlassian.io/jira-agile/boards/backlog#move-issues-to-backlog
+	MoveTo(ctx context.Context, boardID int, payload *models.BoardBacklogPayloadScheme) (*models.ResponseScheme, error)
+}


Test patch:
diff --git a/jira/agile/internal/board_backlog_impl_test.go b/jira/agile/internal/board_backlog_impl_test.go
new file mode 100644
index 0000000..3c55660
--- /dev/null
+++ b/jira/agile/internal/board_backlog_impl_test.go
@@ -0,0 +1,315 @@
+package internal
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/mocks"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+)
+
+func Test_internalBoardBacklogImpl_Move(t *testing.T) {
+
+	payloadMocked := &struct {
+		Issues []string `json:"issues"`
+	}{
+		Issues: []string{"DUMMY-1", "12323"},
+	}
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx    context.Context
+		issues []string
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:    context.Background(),
+				issues: []string{"DUMMY-1", "12323"},
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/agile/1.0/backlog/issue",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the api cannot be executed",
+			args: args{
+				ctx:    context.Background(),
+				issues: []string{"DUMMY-1", "12323"},
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/agile/1.0/backlog/issue",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, errors.New("error, unable to execute the http call"))
+
+				fields.c = client
+			},
+			Err:     errors.New("error, unable to execute the http call"),
+			wantErr: true,
+		},
+
+		{
+			name: "when the request cannot be created",
+			args: args{
+				ctx:    context.Background(),
+				issues: []string{"DUMMY-1", "12323"},
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/agile/1.0/backlog/issue",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, errors.New("unable to create the http request"))
+
+				fields.c = client
+			},
+			Err:     errors.New("unable to create the http request"),
+			wantErr: true,
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			service := NewBoardBacklogService(testCase.fields.c, "1.0")
+
+			gotResponse, err := service.Move(testCase.args.ctx, testCase.args.issues)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+			}
+		})
+	}
+}
+
+func Test_internalBoardBacklogImpl_MoveTo(t *testing.T) {
+
+	payloadMocked := &model.BoardBacklogPayloadScheme{
+		Issues:            []string{"PR-1", "10001", "PR-3"},
+		RankBeforeIssue:   "PR-4",
+		RankAfterIssue:    "",
+		RankCustomFieldId: 10521,
+	}
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx     context.Context
+		boardID int
+		payload *model.BoardBacklogPayloadScheme
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:     context.Background(),
+				boardID: 56,
+				payload: payloadMocked,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/agile/1.0/backlog/56/issue",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the api cannot be executed",
+			args: args{
+				ctx:     context.Background(),
+				boardID: 56,
+				payload: payloadMocked,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/agile/1.0/backlog/56/issue",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, errors.New("error, unable to execute the http call"))
+
+				fields.c = client
+			},
+			Err:     errors.New("error, unable to execute the http call"),
+			wantErr: true,
+		},
+
+		{
+			name: "when the request cannot be created",
+			args: args{
+				ctx:     context.Background(),
+				boardID: 56,
+				payload: payloadMocked,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/agile/1.0/backlog/56/issue",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, errors.New("unable to create the http request"))
+
+				fields.c = client
+			},
+			Err:     errors.New("unable to create the http request"),
+			wantErr: true,
+		},
+
+		{
+			name: "when the board id is not provided",
+			args: args{
+				ctx: context.Background(),
+			},
+			Err:     model.ErrNoBoardIDError,
+			wantErr: true,
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			service := NewBoardBacklogService(testCase.fields.c, "1.0")
+
+			gotResponse, err := service.MoveTo(testCase.args.ctx, testCase.args.boardID, testCase.args.payload)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+			}
+		})
+	}
+}