Repository: ctreminiom/go-atlassian
Pull request #102 (2022-01-30 19:06:15)

dev


Patch:
diff --git a/README.md b/README.md
index abf0e41..dbfa1b7 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@
     <a href="https://github.com/ctreminiom/go-atlassian/actions?query=workflow%3ATesting"><img src="https://img.shields.io/github/workflow/status/ctreminiom/go-atlassian/Testing?label=%F0%9F%A7%AA%20tests&style=flat&color=75C46B"></a>
     <a href="https://docs.go-atlassian.io/"><img src="https://img.shields.io/badge/%F0%9F%92%A1%20go-documentation-00ACD7.svg?style=flat"></a>
     <a href="https://bestpractices.coreinfrastructure.org/projects/4861"><img src="https://bestpractices.coreinfrastructure.org/projects/4861/badge"></a> 
+    <a href="https://github.com/avelino/awesome-go#third-party-apis"><img src="https://awesome.re/mentioned-badge-flat.svg"></a> 
 </p>
 
 Communicate with the [Atlassian API's](https://developer.atlassian.com/cloud/) quickly and easily
diff --git a/jira/mocks/get-project-features.json b/jira/mocks/get-project-features.json
new file mode 100644
index 0000000..fe3ee32
--- /dev/null
+++ b/jira/mocks/get-project-features.json
@@ -0,0 +1,24 @@
+{
+  "features": [
+    {
+      "projectId": 10001,
+      "state": "ENABLED",
+      "toggleLocked": true,
+      "feature": "jsw.classic.roadmap",
+      "prerequisites": [],
+      "localisedName": "Roadmap",
+      "localisedDescription": "Your roadmap is an optimized location to create and manage your epics.",
+      "imageUri": "https://jira.atlassian.com/s/sb53l8/b/3/ab8a7691e4738b4f147e293f0864adfd5b8d3c85/_/download/resources/com.atlassian.jira.rest:classic-project-features/simple-roadmap-feature.svg"
+    },
+    {
+      "projectId": 10001,
+      "state": "ENABLED",
+      "toggleLocked": true,
+      "feature": "jsw.classic.backlog",
+      "prerequisites": [],
+      "localisedName": "Backlog",
+      "localisedDescription": "Plan and prioritize work in a dedicated space. To enable and configure the backlog for each board, go to board settings.",
+      "imageUri": "https://jira.atlassian.com/s/sb53l8/b/3/ab8a7691e4738b4f147e293f0864adfd5b8d3c85/_/download/resources/com.atlassian.jira.rest:classic-project-features/simple-backlog-feature.svg"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/jira/v2/jira.go b/jira/v2/jira.go
index 41f1ae1..bf948bc 100644
--- a/jira/v2/jira.go
+++ b/jira/v2/jira.go
@@ -145,6 +145,7 @@ func New(httpClient *http.Client, site string) (client *Client, err error) {
 
 		Type:    &ProjectTypeService{client: client},
 		Version: &ProjectVersionService{client: client},
+		Feature: &ProjectFeatureService{client: client},
 	}
 
 	client.User = &UserService{
diff --git a/jira/v2/project.go b/jira/v2/project.go
index fe105f6..9c20589 100644
--- a/jira/v2/project.go
+++ b/jira/v2/project.go
@@ -19,6 +19,7 @@ type ProjectService struct {
 	Role       *ProjectRoleService
 	Type       *ProjectTypeService
 	Version    *ProjectVersionService
+	Feature    *ProjectFeatureService
 }
 
 // Create creates a project based on a project type template, as shown in the following table:
diff --git a/jira/v2/projectFeature.go b/jira/v2/projectFeature.go
new file mode 100644
index 0000000..0a73b2b
--- /dev/null
+++ b/jira/v2/projectFeature.go
@@ -0,0 +1,73 @@
+package v2
+
+import (
+	"context"
+	"fmt"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"net/http"
+)
+
+type ProjectFeatureService struct{ client *Client }
+
+// Gets returns the list of features for a project.
+// Docs: https://docs.go-atlassian.io/jira-software-cloud/projects/features#get-project-features
+func (p *ProjectFeatureService) Gets(ctx context.Context, projectKeyOrID string) (result *models.ProjectFeaturesScheme, response *ResponseScheme, err error) {
+
+	if projectKeyOrID == "" {
+		return nil, nil, models.ErrNoProjectIDError
+	}
+
+	var endpoint = fmt.Sprintf("rest/api/2/project/%v/features", projectKeyOrID)
+
+	request, err := p.client.newRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return
+	}
+
+	request.Header.Set("Accept", "application/json")
+
+	response, err = p.client.call(request, &result)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+// Set sets the state of a project feature.
+// Docs: https://docs.go-atlassian.io/jira-software-cloud/projects/features#set-project-feature-state
+func (p *ProjectFeatureService) Set(ctx context.Context, projectKeyOrID, featureKey, state string) (result *models.ProjectFeaturesScheme, response *ResponseScheme, err error) {
+
+	if projectKeyOrID == "" {
+		return nil, nil, models.ErrNoProjectIDError
+	}
+
+	if featureKey == "" {
+		return nil, nil, models.ErrNoProjectFeatureKeyError
+	}
+
+	payload := struct {
+		State string `json:"state,omitempty"`
+	}{
+		State: state,
+	}
+
+	payloadAsReader, _ := transformStructToReader(&payload)
+
+	var endpoint = fmt.Sprintf("rest/api/2/project/%v/features/%v", projectKeyOrID, featureKey)
+
+	request, err := p.client.newRequest(ctx, http.MethodPut, endpoint, payloadAsReader)
+	if err != nil {
+		return
+	}
+
+	request.Header.Set("Accept", "application/json")
+	request.Header.Set("Content-Type", "application/json")
+
+	response, err = p.client.call(request, &result)
+	if err != nil {
+		return
+	}
+
+	return
+}
diff --git a/jira/v3/jira.go b/jira/v3/jira.go
index 8df437a..21310f3 100644
--- a/jira/v3/jira.go
+++ b/jira/v3/jira.go
@@ -147,6 +147,7 @@ func New(httpClient *http.Client, site string) (client *Client, err error) {
 
 		Type:    &ProjectTypeService{client: client},
 		Version: &ProjectVersionService{client: client},
+		Feature: &ProjectFeatureService{client: client},
 	}
 
 	client.User = &UserService{
diff --git a/jira/v3/project.go b/jira/v3/project.go
index 59eb2fb..8bb08dc 100644
--- a/jira/v3/project.go
+++ b/jira/v3/project.go
@@ -19,6 +19,7 @@ type ProjectService struct {
 	Role       *ProjectRoleService
 	Type       *ProjectTypeService
 	Version    *ProjectVersionService
+	Feature    *ProjectFeatureService
 }
 
 // Create creates a project based on a project type template, as shown in the following table:
diff --git a/jira/v3/projectFeature.go b/jira/v3/projectFeature.go
new file mode 100644
index 0000000..668d5ee
--- /dev/null
+++ b/jira/v3/projectFeature.go
@@ -0,0 +1,73 @@
+package v3
+
+import (
+	"context"
+	"fmt"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"net/http"
+)
+
+type ProjectFeatureService struct{ client *Client }
+
+// Gets returns the list of features for a project.
+// Docs: https://docs.go-atlassian.io/jira-software-cloud/projects/features#get-project-features
+func (p *ProjectFeatureService) Gets(ctx context.Context, projectKeyOrID string) (result *models.ProjectFeaturesScheme, response *ResponseScheme, err error) {
+
+	if projectKeyOrID == "" {
+		return nil, nil, models.ErrNoProjectIDError
+	}
+
+	var endpoint = fmt.Sprintf("rest/api/3/project/%v/features", projectKeyOrID)
+
+	request, err := p.client.newRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return
+	}
+
+	request.Header.Set("Accept", "application/json")
+
+	response, err = p.client.call(request, &result)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+// Set sets the state of a project feature.
+// Docs: https://docs.go-atlassian.io/jira-software-cloud/projects/features#set-project-feature-state
+func (p *ProjectFeatureService) Set(ctx context.Context, projectKeyOrID, featureKey, state string) (result *models.ProjectFeaturesScheme, response *ResponseScheme, err error) {
+
+	if projectKeyOrID == "" {
+		return nil, nil, models.ErrNoProjectIDError
+	}
+
+	if featureKey == "" {
+		return nil, nil, models.ErrNoProjectFeatureKeyError
+	}
+
+	payload := struct {
+		State string `json:"state,omitempty"`
+	}{
+		State: state,
+	}
+
+	payloadAsReader, _ := transformStructToReader(&payload)
+
+	var endpoint = fmt.Sprintf("rest/api/3/project/%v/features/%v", projectKeyOrID, featureKey)
+
+	request, err := p.client.newRequest(ctx, http.MethodPut, endpoint, payloadAsReader)
+	if err != nil {
+		return
+	}
+
+	request.Header.Set("Accept", "application/json")
+	request.Header.Set("Content-Type", "application/json")
+
+	response, err = p.client.call(request, &result)
+	if err != nil {
+		return
+	}
+
+	return
+}
diff --git a/pkg/infra/models/confluence_content.go b/pkg/infra/models/confluence_content.go
index c78e1a3..1942ee4 100644
--- a/pkg/infra/models/confluence_content.go
+++ b/pkg/infra/models/confluence_content.go
@@ -46,6 +46,7 @@ type ContentScheme struct {
 	Version    *ContentVersionScheme   `json:"version,omitempty"`
 	Extensions *ContentExtensionScheme `json:"extensions,omitempty"`
 	Ancestors  []*ContentScheme        `json:"ancestors,omitempty"`
+	History    *ContentHistoryScheme   `json:"history,omitempty"`
 }
 
 type ContentExtensionScheme struct {
@@ -141,10 +142,10 @@ type ContentHistoryScheme struct {
 	Latest          bool                              `json:"latest,omitempty"`
 	CreatedBy       *ContentUserScheme                `json:"createdBy,omitempty"`
 	CreatedDate     string                            `json:"createdDate,omitempty"`
-	LastUpdated     *VersionScheme                    `json:"lastUpdated,omitempty"`
-	PreviousVersion *VersionScheme                    `json:"previousVersion,omitempty"`
+	LastUpdated     *ContentVersionScheme             `json:"lastUpdated,omitempty"`
+	PreviousVersion *ContentVersionScheme             `json:"previousVersion,omitempty"`
 	Contributors    *ContentHistoryContributorsScheme `json:"contributors,omitempty"`
-	NextVersion     *VersionScheme                    `json:"nextVersion,omitempty"`
+	NextVersion     *ContentVersionScheme             `json:"nextVersion,omitempty"`
 	Expandable      *ExpandableScheme                 `json:"_expandable,omitempty"`
 	Links           *LinkScheme                       `json:"_links,omitempty"`
 }
diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go
index e7e8293..e9991f5 100644
--- a/pkg/infra/models/errors.go
+++ b/pkg/infra/models/errors.go
@@ -56,6 +56,7 @@ var (
 	ErrNoReaderError                       = errors.New("jira: no reader set")
 	ErrNoCommentIDError                    = errors.New("jira: no comment id set")
 	ErrNoProjectIDError                    = errors.New("jira: no project id set")
+	ErrNoProjectFeatureKeyError            = errors.New("jira: no project feature key set")
 	ErrNoFieldIDError                      = errors.New("jira: no field id set")
 	ErrNoFieldContextIDError               = errors.New("jira: no field context id set")
 	ErrNoIssueTypesError                   = errors.New("jira: no issue types id's set")
diff --git a/pkg/infra/models/jira_project_feature.go b/pkg/infra/models/jira_project_feature.go
new file mode 100644
index 0000000..8bf0ca4
--- /dev/null
+++ b/pkg/infra/models/jira_project_feature.go
@@ -0,0 +1,16 @@
+package models
+
+type ProjectFeaturesScheme struct {
+	Features []*ProjectFeatureScheme `json:"features,omitempty"`
+}
+
+type ProjectFeatureScheme struct {
+	ProjectID            int      `json:"projectId,omitempty"`
+	State                string   `json:"state,omitempty"`
+	ToggleLocked         bool     `json:"toggleLocked,omitempty"`
+	Feature              string   `json:"feature,omitempty"`
+	Prerequisites        []string `json:"prerequisites,omitempty"`
+	LocalisedName        string   `json:"localisedName,omitempty"`
+	LocalisedDescription string   `json:"localisedDescription,omitempty"`
+	ImageURI             string   `json:"imageUri,omitempty"`
+}


Test patch:
diff --git a/jira/v2/projectFeature_test.go b/jira/v2/projectFeature_test.go
new file mode 100644
index 0000000..01aedb4
--- /dev/null
+++ b/jira/v2/projectFeature_test.go
@@ -0,0 +1,313 @@
+package v2
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/url"
+	"testing"
+)
+
+func Test_ProjectFeatureService_Gets(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		projectKeyOrID     string
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name:               "when the parameters are correct",
+			projectKeyOrID:     "DUMMY",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/2/project/DUMMY/features",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+		},
+		{
+			name:               "when the project key or id not provided",
+			projectKeyOrID:     "",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/2/project/DUMMY/features",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "jira: no project id set",
+		},
+		{
+			name:               "when the context is not provided",
+			projectKeyOrID:     "DUMMY",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/2/project/DUMMY/features",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name:               "when the response status is incorrect",
+			projectKeyOrID:     "DUMMY",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/2/project/DUMMY/features",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusBadRequest,
+			wantErr:            true,
+			expectedError:      "request failed. Please analyze the request body for more details. Status Code: 400",
+		},
+
+		{
+			name:               "when the response body is empty",
+			projectKeyOrID:     "DUMMY",
+			mockFile:           "../mocks/empty-json.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/2/project/DUMMY/features",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "unexpected end of JSON input",
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			//Init a new HTTP mock server
+			mockOptions := mockServerOptions{
+				Endpoint:           testCase.endpoint,
+				MockFilePath:       testCase.mockFile,
+				MethodAccepted:     testCase.wantHTTPMethod,
+				ResponseCodeWanted: testCase.wantHTTPCodeReturn,
+			}
+
+			mockServer, err := startMockServer(&mockOptions)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			defer mockServer.Close()
+
+			//Init the library instance
+			mockClient, err := startMockClient(mockServer.URL)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			i := &ProjectFeatureService{client: mockClient}
+
+			gotResult, gotResponse, err := i.Gets(testCase.context, testCase.projectKeyOrID)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.expectedError)
+
+				if gotResponse != nil {
+					t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code)
+				}
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+
+				apiEndpoint, err := url.Parse(gotResponse.Endpoint)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				var endpointToAssert string
+
+				if apiEndpoint.Query().Encode() != "" {
+					endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode())
+				} else {
+					endpointToAssert = apiEndpoint.Path
+				}
+
+				t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert)
+				assert.Equal(t, testCase.endpoint, endpointToAssert)
+			}
+		})
+
+	}
+
+}
+
+func Test_ProjectFeatureService_Set(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		projectKeyOrID     string
+		featureKey, state  string
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name:               "when the parameters are correct",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/2/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+		},
+
+		{
+			name:               "when the feature key is not provided",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/2/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "jira: no project feature key set",
+		},
+		{
+			name:               "when the project key or id not provided",
+			projectKeyOrID:     "",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/2/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "jira: no project id set",
+		},
+		{
+			name:               "when the context is not provided",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/2/project/DUMMY/features/jsw.classic.roadmap",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name:               "when the response status is incorrect",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/2/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusBadRequest,
+			wantErr:            true,
+			expectedError:      "request failed. Please analyze the request body for more details. Status Code: 400",
+		},
+
+		{
+			name:               "when the response body is empty",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/empty-json.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/2/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "unexpected end of JSON input",
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			//Init a new HTTP mock server
+			mockOptions := mockServerOptions{
+				Endpoint:           testCase.endpoint,
+				MockFilePath:       testCase.mockFile,
+				MethodAccepted:     testCase.wantHTTPMethod,
+				ResponseCodeWanted: testCase.wantHTTPCodeReturn,
+			}
+
+			mockServer, err := startMockServer(&mockOptions)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			defer mockServer.Close()
+
+			//Init the library instance
+			mockClient, err := startMockClient(mockServer.URL)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			i := &ProjectFeatureService{client: mockClient}
+
+			gotResult, gotResponse, err := i.Set(testCase.context, testCase.projectKeyOrID, testCase.featureKey, testCase.state)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.expectedError)
+
+				if gotResponse != nil {
+					t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code)
+				}
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+
+				apiEndpoint, err := url.Parse(gotResponse.Endpoint)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				var endpointToAssert string
+
+				if apiEndpoint.Query().Encode() != "" {
+					endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode())
+				} else {
+					endpointToAssert = apiEndpoint.Path
+				}
+
+				t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert)
+				assert.Equal(t, testCase.endpoint, endpointToAssert)
+			}
+		})
+
+	}
+
+}
diff --git a/jira/v3/projectFeature_test.go b/jira/v3/projectFeature_test.go
new file mode 100644
index 0000000..1668156
--- /dev/null
+++ b/jira/v3/projectFeature_test.go
@@ -0,0 +1,313 @@
+package v3
+
+import (
+	"context"
+	"fmt"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/url"
+	"testing"
+)
+
+func Test_ProjectFeatureService_Gets(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		projectKeyOrID     string
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name:               "when the parameters are correct",
+			projectKeyOrID:     "DUMMY",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/3/project/DUMMY/features",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+		},
+		{
+			name:               "when the project key or id not provided",
+			projectKeyOrID:     "",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/3/project/DUMMY/features",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "jira: no project id set",
+		},
+		{
+			name:               "when the context is not provided",
+			projectKeyOrID:     "DUMMY",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/3/project/DUMMY/features",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name:               "when the response status is incorrect",
+			projectKeyOrID:     "DUMMY",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/3/project/DUMMY/features",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusBadRequest,
+			wantErr:            true,
+			expectedError:      "request failed. Please analyze the request body for more details. Status Code: 400",
+		},
+
+		{
+			name:               "when the response body is empty",
+			projectKeyOrID:     "DUMMY",
+			mockFile:           "../mocks/empty-json.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/rest/api/3/project/DUMMY/features",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "unexpected end of JSON input",
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			//Init a new HTTP mock server
+			mockOptions := mockServerOptions{
+				Endpoint:           testCase.endpoint,
+				MockFilePath:       testCase.mockFile,
+				MethodAccepted:     testCase.wantHTTPMethod,
+				ResponseCodeWanted: testCase.wantHTTPCodeReturn,
+			}
+
+			mockServer, err := startMockServer(&mockOptions)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			defer mockServer.Close()
+
+			//Init the library instance
+			mockClient, err := startMockClient(mockServer.URL)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			i := &ProjectFeatureService{client: mockClient}
+
+			gotResult, gotResponse, err := i.Gets(testCase.context, testCase.projectKeyOrID)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.expectedError)
+
+				if gotResponse != nil {
+					t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code)
+				}
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+
+				apiEndpoint, err := url.Parse(gotResponse.Endpoint)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				var endpointToAssert string
+
+				if apiEndpoint.Query().Encode() != "" {
+					endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode())
+				} else {
+					endpointToAssert = apiEndpoint.Path
+				}
+
+				t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert)
+				assert.Equal(t, testCase.endpoint, endpointToAssert)
+			}
+		})
+
+	}
+
+}
+
+func Test_ProjectFeatureService_Set(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		projectKeyOrID     string
+		featureKey, state  string
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name:               "when the parameters are correct",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/3/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+		},
+
+		{
+			name:               "when the feature key is not provided",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/3/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "jira: no project feature key set",
+		},
+		{
+			name:               "when the project key or id not provided",
+			projectKeyOrID:     "",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/3/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "jira: no project id set",
+		},
+		{
+			name:               "when the context is not provided",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/3/project/DUMMY/features/jsw.classic.roadmap",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name:               "when the response status is incorrect",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/get-project-features.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/3/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusBadRequest,
+			wantErr:            true,
+			expectedError:      "request failed. Please analyze the request body for more details. Status Code: 400",
+		},
+
+		{
+			name:               "when the response body is empty",
+			projectKeyOrID:     "DUMMY",
+			featureKey:         "jsw.classic.roadmap",
+			state:              "ENABLED",
+			mockFile:           "../mocks/empty-json.json",
+			wantHTTPMethod:     http.MethodPut,
+			endpoint:           "/rest/api/3/project/DUMMY/features/jsw.classic.roadmap",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "unexpected end of JSON input",
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			//Init a new HTTP mock server
+			mockOptions := mockServerOptions{
+				Endpoint:           testCase.endpoint,
+				MockFilePath:       testCase.mockFile,
+				MethodAccepted:     testCase.wantHTTPMethod,
+				ResponseCodeWanted: testCase.wantHTTPCodeReturn,
+			}
+
+			mockServer, err := startMockServer(&mockOptions)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			defer mockServer.Close()
+
+			//Init the library instance
+			mockClient, err := startMockClient(mockServer.URL)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			i := &ProjectFeatureService{client: mockClient}
+
+			gotResult, gotResponse, err := i.Set(testCase.context, testCase.projectKeyOrID, testCase.featureKey, testCase.state)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.expectedError)
+
+				if gotResponse != nil {
+					t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code)
+				}
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+
+				apiEndpoint, err := url.Parse(gotResponse.Endpoint)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				var endpointToAssert string
+
+				if apiEndpoint.Query().Encode() != "" {
+					endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode())
+				} else {
+					endpointToAssert = apiEndpoint.Path
+				}
+
+				t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert)
+				assert.Equal(t, testCase.endpoint, endpointToAssert)
+			}
+		})
+
+	}
+
+}