From 87f156863191e5b310df226f4c0b9c71d28129cf Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Sat, 5 Oct 2024 18:36:44 -0600 Subject: [PATCH] :sparkles: Mapped the new issue.Metadata methods. - Resolves: #319 - Updated the README.md and added a new section to execute a RAW endpoint - Created the documentation links: - https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-metadata-issue-types-for-a-project - https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-field-metadata-for-a-project-and-issue-type-id --- README.md | 61 ++++- jira/internal/metadata_impl.go | 107 +++++++++ jira/internal/metadata_impl_test.go | 360 ++++++++++++++++++++++++++++ service/jira/metadata.go | 45 ++++ 4 files changed, 572 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eeb38c34..e020f300 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ instance.Auth.SetBasicAuth("YOUR_CLIENT_MAIL", "YOUR_APP_ACCESS_TOKEN") ## ☕Cookbooks -For detailed examples and usage of the go-atlassian library, please refer to our Cookbook. This section provides step-by-step guides and code samples for common tasks and scenarios. +For detailed examples and usage of the go-atlassian library, please refer to our [**Cookbook**](https://docs.go-atlassian.io/cookbooks). This section provides step-by-step guides and code samples for common tasks and scenarios. ------------------------- ## 🌍 Services @@ -161,6 +161,65 @@ for _, transition := range issue.Transitions { The rest of the service functions work much the same way; they are concise and behave as you would expect. The [documentation](https://docs.go-atlassian.io/) contains several examples on how to use each service function. + +## 📪Call a RAW API Endpoint +If you need to interact with an Atlassian API endpoint that hasn't been implemented in the `go-atlassian` library yet, you can make a custom API request using the built-in `Client.Call` method to execute raw HTTP requests. + +> Please raise an issue in order to implement the endpoint + +```go +package main + +import ( + "context" + "fmt" + "github.com/ctreminiom/go-atlassian/jira/v3" + "log" + "net/http" + "os" + ) + +type IssueTypeMetadata struct { + IssueTypes []struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + } `json:"issueTypes"` +} + +func main() { + + var ( + host = os.Getenv("SITE") + mail = os.Getenv("MAIL") + token = os.Getenv("TOKEN") + ) + + atlassian, err := v3.New(nil, host) + if err != nil { + log.Fatal(err) + } + + atlassian.Auth.SetBasicAuth(mail, token) + + // Define the RAW endpoint + apiEndpoint := "rest/api/3/issue/createmeta/KP/issuetypes" + + request, err := atlassian.NewRequest(context.Background(), http.MethodGet, apiEndpoint, "", nil) + if err != nil { + log.Fatal(err) + } + + customResponseStruct := new(IssueTypeMetadata) + response, err := atlassian.Call(request, &customResponseStruct) + if err != nil { + log.Fatal(err) + } + + fmt.Println(response.Status) +} +``` + ------------------------- ## ✍️ Contributions diff --git a/jira/internal/metadata_impl.go b/jira/internal/metadata_impl.go index b43622a9..798d9c53 100644 --- a/jira/internal/metadata_impl.go +++ b/jira/internal/metadata_impl.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "github.com/tidwall/gjson" @@ -33,6 +34,8 @@ type MetadataService struct { // Get edit issue metadata returns the edit screen fields for an issue that are visible to and editable by the user. // +// Deprecated. Please use Issue.Metadata.FetchIssueMappings() and Issue.Metadata.FetchFieldMappings() instead. +// // Use the information to populate the requests in Edit issue. // // GET /rest/api/{2-3}/issue/{issueKeyOrID}/editmeta @@ -44,6 +47,8 @@ func (m *MetadataService) Get(ctx context.Context, issueKeyOrID string, override // Create returns details of projects, issue types within projects, and, when requested, // +// Deprecated. Please use Issue.Metadata.FetchIssueMappings() and Issue.Metadata.FetchFieldMappings() instead. +// // the create screen fields for each issue type for the user. // // GET /rest/api/{2-3}/issue/createmeta @@ -53,11 +58,113 @@ func (m *MetadataService) Create(ctx context.Context, opts *model.IssueMetadataC return m.internalClient.Create(ctx, opts) } +// FetchIssueMappings returns a page of issue type metadata for a specified project. +// +// Use the information to populate the requests in Create issue and Create issues. +// +// This operation can be accessed anonymously. +// +// GET /rest/api/{2-3}/issue/createmeta/{projectIdOrKey}/issuetypes +// +// Parameters: +// - ctx: The context for the request. +// - projectKeyOrID: The key or ID of the project. +// - startAt: The starting index of the returned issues. +// - maxResults: The maximum number of results to return. +// +// Returns: +// - A gjson.Result containing the issue type metadata. +// - A pointer to the response scheme. +// - An error if the retrieval fails. +// +// https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-metadata-issue-types-for-a-project +func (m *MetadataService) FetchIssueMappings(ctx context.Context, projectKeyOrID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) { + return m.internalClient.FetchIssueMappings(ctx, projectKeyOrID, startAt, maxResults) +} + +// FetchFieldMappings returns a page of field metadata for a specified project and issue type. +// +// Use the information to populate the requests in Create issue and Create issues. +// +// This operation can be accessed anonymously. +// +// GET /rest/api/{2-3}/issue/createmeta/{projectIdOrKey}/fields/{issueTypeId} +// +// Parameters: +// - ctx: The context for the request. +// - projectKeyOrID: The key or ID of the project. +// - issueTypeID: The ID of the issue type whose metadata is to be retrieved. +// - startAt: The starting index of the returned fields. +// - maxResults: The maximum number of results to return. +// +// Returns: +// - A gjson.Result containing the field metadata. +// - A pointer to the response scheme. +// - An error if the retrieval fails. +// +// https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-field-metadata-for-a-project-and-issue-type-id +func (m *MetadataService) FetchFieldMappings(ctx context.Context, projectKeyOrID, issueTypeID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) { + return m.internalClient.FetchFieldMappings(ctx, projectKeyOrID, issueTypeID, startAt, maxResults) +} + type internalMetadataImpl struct { c service.Connector version string } +func (i *internalMetadataImpl) FetchIssueMappings(ctx context.Context, projectKeyOrID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) { + + if projectKeyOrID == "" { + return gjson.Result{}, nil, model.ErrNoProjectIDOrKey + } + + params := url.Values{} + params.Add("startAt", strconv.Itoa(startAt)) + params.Add("maxResults", strconv.Itoa(maxResults)) + + endpoint := fmt.Sprintf("rest/api/%v/issue/createmeta/%v/issuetypes?%v", i.version, projectKeyOrID, params.Encode()) + + request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil) + if err != nil { + return gjson.Result{}, nil, err + } + + response, err := i.c.Call(request, nil) + if err != nil { + return gjson.Result{}, response, err + } + + return gjson.ParseBytes(response.Bytes.Bytes()), response, nil +} + +func (i *internalMetadataImpl) FetchFieldMappings(ctx context.Context, projectKeyOrID, issueTypeID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) { + + if projectKeyOrID == "" { + return gjson.Result{}, nil, model.ErrNoProjectIDOrKey + } + + if issueTypeID == "" { + return gjson.Result{}, nil, model.ErrNoIssueTypeID + } + + params := url.Values{} + params.Add("startAt", strconv.Itoa(startAt)) + params.Add("maxResults", strconv.Itoa(maxResults)) + endpoint := fmt.Sprintf("rest/api/%v/issue/createmeta/%v/issuetypes/%v?%v", i.version, projectKeyOrID, issueTypeID, params.Encode()) + + request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil) + if err != nil { + return gjson.Result{}, nil, err + } + + response, err := i.c.Call(request, nil) + if err != nil { + return gjson.Result{}, response, err + } + + return gjson.ParseBytes(response.Bytes.Bytes()), response, nil +} + func (i *internalMetadataImpl) Get(ctx context.Context, issueKeyOrID string, overrideScreenSecurity, overrideEditableFlag bool) (gjson.Result, *model.ResponseScheme, error) { if issueKeyOrID == "" { diff --git a/jira/internal/metadata_impl_test.go b/jira/internal/metadata_impl_test.go index b4bd3fc7..9fb976fd 100644 --- a/jira/internal/metadata_impl_test.go +++ b/jira/internal/metadata_impl_test.go @@ -385,3 +385,363 @@ func Test_NewMetadataService(t *testing.T) { }) } } + +func Test_internalMetadataImpl_FetchFieldMappings(t *testing.T) { + type fields struct { + c service.Connector + version string + } + type args struct { + ctx context.Context + projectKeyOrID string + issueTypeID string + startAt int + maxResults int + } + tests := []struct { + name string + fields fields + args args + on func(*fields) + want gjson.Result + wantErr bool + Err error + }{ + { + name: "when the project key or ID is not provided", + fields: fields{version: "3"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "", + issueTypeID: "10001", + startAt: 0, + maxResults: 50, + }, + want: gjson.Result{}, + wantErr: true, + Err: model.ErrNoProjectIDOrKey, + }, + { + name: "when the issue type ID is not provided", + fields: fields{version: "3"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + issueTypeID: "", + startAt: 0, + maxResults: 50, + }, + want: gjson.Result{}, + wantErr: true, + Err: model.ErrNoIssueTypeID, + }, + { + name: "when the API version is v3", + fields: fields{version: "3"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + issueTypeID: "10001", + startAt: 0, + maxResults: 50, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + client.On("NewRequest", + context.Background(), + http.MethodGet, + "rest/api/3/issue/createmeta/DUMMY/issuetypes/10001?maxResults=50&startAt=0", + "", + nil). + Return(&http.Request{}, nil) + client.On("Call", + &http.Request{}, + nil). + Return(&model.ResponseScheme{}, nil) + fields.c = client + }, + want: gjson.Result{}, + wantErr: false, + }, + { + name: "when the API version is v2", + fields: fields{version: "2"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + issueTypeID: "10001", + startAt: 0, + maxResults: 50, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + client.On("NewRequest", + context.Background(), + http.MethodGet, + "rest/api/2/issue/createmeta/DUMMY/issuetypes/10001?maxResults=50&startAt=0", + "", + nil). + Return(&http.Request{}, nil) + client.On("Call", + &http.Request{}, + nil). + Return(&model.ResponseScheme{}, nil) + fields.c = client + }, + want: gjson.Result{}, + wantErr: false, + }, + { + name: "when the HTTP request cannot be created", + fields: fields{version: "2"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + issueTypeID: "10001", + startAt: 0, + maxResults: 50, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + client.On("NewRequest", + context.Background(), + http.MethodGet, + "rest/api/2/issue/createmeta/DUMMY/issuetypes/10001?maxResults=50&startAt=0", + "", + nil). + Return(&http.Request{}, errors.New("error")) + fields.c = client + }, + want: gjson.Result{}, + wantErr: true, + Err: errors.New("error"), + }, + { + name: "when the HTTP call fails", + fields: fields{version: "2"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + issueTypeID: "10001", + startAt: 0, + maxResults: 50, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + client.On("NewRequest", + context.Background(), + http.MethodGet, + "rest/api/2/issue/createmeta/DUMMY/issuetypes/10001?maxResults=50&startAt=0", + "", + nil). + Return(&http.Request{}, nil) + client.On("Call", + &http.Request{}, + nil). + Return(nil, errors.New("error")) + fields.c = client + }, + want: gjson.Result{}, + wantErr: true, + Err: errors.New("error"), + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + + if testCase.on != nil { + testCase.on(&testCase.fields) + } + + metadataService, err := NewMetadataService(testCase.fields.c, testCase.fields.version) + assert.NoError(t, err) + + gotResult, gotResponse, err := metadataService.FetchFieldMappings(testCase.args.ctx, testCase.args.projectKeyOrID, testCase.args.issueTypeID, testCase.args.startAt, testCase.args.maxResults) + 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) + assert.NotEqual(t, gotResult, nil) + assert.Equal(t, gotResult, testCase.want) + } + }) + } +} + +func Test_internalMetadataImpl_FetchIssueMappings(t *testing.T) { + type fields struct { + c service.Connector + version string + } + type args struct { + ctx context.Context + projectKeyOrID string + startAt int + maxResults int + } + tests := []struct { + name string + fields fields + args args + on func(*fields) + want gjson.Result + wantErr bool + Err error + }{ + { + name: "when the project key or ID is not provided", + fields: fields{version: "3"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "", + startAt: 0, + maxResults: 50, + }, + want: gjson.Result{}, + wantErr: true, + Err: model.ErrNoProjectIDOrKey, + }, + { + name: "when the API version is v3", + fields: fields{version: "3"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + startAt: 0, + maxResults: 50, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + client.On("NewRequest", + context.Background(), + http.MethodGet, + "rest/api/3/issue/createmeta/DUMMY/issuetypes?maxResults=50&startAt=0", + "", + nil). + Return(&http.Request{}, nil) + client.On("Call", + &http.Request{}, + nil). + Return(&model.ResponseScheme{}, nil) + fields.c = client + }, + want: gjson.Result{}, + wantErr: false, + }, + { + name: "when the API version is v2", + fields: fields{version: "2"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + startAt: 0, + maxResults: 50, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + client.On("NewRequest", + context.Background(), + http.MethodGet, + "rest/api/2/issue/createmeta/DUMMY/issuetypes?maxResults=50&startAt=0", + "", + nil). + Return(&http.Request{}, nil) + client.On("Call", + &http.Request{}, + nil). + Return(&model.ResponseScheme{}, nil) + fields.c = client + }, + want: gjson.Result{}, + wantErr: false, + }, + { + name: "when the HTTP request cannot be created", + fields: fields{version: "2"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + startAt: 0, + maxResults: 50, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + client.On("NewRequest", + context.Background(), + http.MethodGet, + "rest/api/2/issue/createmeta/DUMMY/issuetypes?maxResults=50&startAt=0", + "", + nil). + Return(&http.Request{}, errors.New("error")) + fields.c = client + }, + want: gjson.Result{}, + wantErr: true, + Err: errors.New("error"), + }, + { + name: "when the HTTP call fails", + fields: fields{version: "2"}, + args: args{ + ctx: context.Background(), + projectKeyOrID: "DUMMY", + startAt: 0, + maxResults: 50, + }, + on: func(fields *fields) { + client := mocks.NewConnector(t) + client.On("NewRequest", + context.Background(), + http.MethodGet, + "rest/api/2/issue/createmeta/DUMMY/issuetypes?maxResults=50&startAt=0", + "", + nil). + Return(&http.Request{}, nil) + client.On("Call", + &http.Request{}, + nil). + Return(nil, errors.New("error")) + fields.c = client + }, + want: gjson.Result{}, + wantErr: true, + Err: errors.New("error"), + }, + } + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + + if testCase.on != nil { + testCase.on(&testCase.fields) + } + + metadataService, err := NewMetadataService(testCase.fields.c, testCase.fields.version) + assert.NoError(t, err) + + gotResult, gotResponse, err := metadataService.FetchIssueMappings(testCase.args.ctx, testCase.args.projectKeyOrID, testCase.args.startAt, testCase.args.maxResults) + 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) + assert.NotEqual(t, gotResult, nil) + assert.Equal(t, gotResult, testCase.want) + } + }) + } +} diff --git a/service/jira/metadata.go b/service/jira/metadata.go index bc3b5c29..bd998dc1 100644 --- a/service/jira/metadata.go +++ b/service/jira/metadata.go @@ -27,4 +27,49 @@ type MetadataConnector interface { // // https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-issue-metadata Create(ctx context.Context, opts *model.IssueMetadataCreateOptions) (gjson.Result, *model.ResponseScheme, error) + + // FetchIssueMappings returns a page of issue type metadata for a specified project. + // + // Use the information to populate the requests in Create issue and Create issues. + // + // This operation can be accessed anonymously. + // + // GET /rest/api/{2-3}/issue/createmeta/{projectIdOrKey}/issuetypes + // + // Parameters: + // - ctx: The context for the request. + // - projectKeyOrID: The key or ID of the project. + // - startAt: The starting index of the returned issues. + // - maxResults: The maximum number of results to return. + // + // Returns: + // - A gjson.Result containing the issue type metadata. + // - A pointer to the response scheme. + // - An error if the retrieval fails. + // + // https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-metadata-issue-types-for-a-project + FetchIssueMappings(ctx context.Context, projectKeyOrID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) + + // FetchFieldMappings returns a page of field metadata for a specified project and issue type. + // + // Use the information to populate the requests in Create issue and Create issues. + // + // This operation can be accessed anonymously. + // + // GET /rest/api/{2-3}/issue/createmeta/{projectIdOrKey}/fields/{issueTypeId} + // + // Parameters: + // - ctx: The context for the request. + // - projectKeyOrID: The key or ID of the project. + // - issueTypeID: The ID of the issue type whose metadata is to be retrieved. + // - startAt: The starting index of the returned fields. + // - maxResults: The maximum number of results to return. + // + // Returns: + // - A gjson.Result containing the field metadata. + // - A pointer to the response scheme. + // - An error if the retrieval fails. + // + // https://docs.go-atlassian.io/jira-software-cloud/issues/metadata#get-create-field-metadata-for-a-project-and-issue-type-id + FetchFieldMappings(ctx context.Context, projectKeyOrID, issueTypeID string, startAt, maxResults int) (gjson.Result, *model.ResponseScheme, error) }