1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-11-06 09:09:19 +02:00

[ABAP] Refactor steps to allow API migration (#4687)

* Initial API Manager

* Intermediate part

* Intermediate step

* Fix utils tests

* Adapt pull

* Migrate Checkout

* Refactor createTags

* Refactoring

* Setup tests for SAP_COM_0510

* Add tests

* Refactor parsing

* Add retry to clone

* refactor

* Refactor and tests

* Fix function call

* Adapt create tag tests

* Adapt tests

* Add tests

* Fix tests

* Fix test

* Fix client mock

* Add unit test comments

* Add missing parameters

* Branch not mandatory for clone

* Improve switch branch trigger

---------

Co-authored-by: tiloKo <70266685+tiloKo@users.noreply.github.com>
This commit is contained in:
Daniel Mieg
2023-11-28 13:26:31 +01:00
committed by GitHub
parent 17de9ed34c
commit 0a738e882c
17 changed files with 1485 additions and 1070 deletions

View File

@@ -167,6 +167,9 @@ func ReadConfigFile(path string) (file []byte, err error) {
// GetHTTPResponse wraps the SendRequest function of piperhttp
func GetHTTPResponse(requestType string, connectionDetails ConnectionDetailsHTTP, body []byte, client piperhttp.Sender) (*http.Response, error) {
log.Entry().Debugf("Request body: %s", string(body))
log.Entry().Debugf("Request user: %s", connectionDetails.User)
header := make(map[string][]string)
header["Content-Type"] = []string{"application/json"}
header["Accept"] = []string{"application/json"}
@@ -182,16 +185,20 @@ func GetHTTPResponse(requestType string, connectionDetails ConnectionDetailsHTTP
// Further error details may be present in the response body of the HTTP response.
// If the response body is parseable, the included details are wrapped around the original error from the HTTP repsponse.
// If this is not possible, the original error is returned.
func HandleHTTPError(resp *http.Response, err error, message string, connectionDetails ConnectionDetailsHTTP) error {
func HandleHTTPError(resp *http.Response, err error, message string, connectionDetails ConnectionDetailsHTTP) (string, error) {
var errorText string
var errorCode string
var parsingError error
if resp == nil {
// Response is nil in case of a timeout
log.Entry().WithError(err).WithField("ABAP Endpoint", connectionDetails.URL).Error("Request failed")
match, _ := regexp.MatchString(".*EOF$", err.Error())
if match {
AddDefaultDashedLine()
AddDefaultDashedLine(1)
log.Entry().Infof("%s", "A connection could not be established to the ABAP system. The typical root cause is the network configuration (firewall, IP allowlist, etc.)")
AddDefaultDashedLine()
AddDefaultDashedLine(1)
}
log.Entry().Infof("Error message: %s,", err.Error())
@@ -201,15 +208,15 @@ func HandleHTTPError(resp *http.Response, err error, message string, connectionD
log.Entry().WithField("StatusCode", resp.Status).WithField("User", connectionDetails.User).WithField("URL", connectionDetails.URL).Error(message)
errorText, errorCode, parsingError := GetErrorDetailsFromResponse(resp)
errorText, errorCode, parsingError = GetErrorDetailsFromResponse(resp)
if parsingError != nil {
return err
return "", err
}
abapError := errors.New(fmt.Sprintf("%s - %s", errorCode, errorText))
err = errors.Wrap(abapError, err.Error())
}
return err
return errorCode, err
}
func GetErrorDetailsFromResponse(resp *http.Response) (errorString string, errorCode string, err error) {
@@ -249,8 +256,10 @@ func ConvertTime(logTimeStamp string) time.Time {
}
// AddDefaultDashedLine adds 25 dashes
func AddDefaultDashedLine() {
log.Entry().Infof(strings.Repeat("-", 25))
func AddDefaultDashedLine(j int) {
for i := 1; i <= j; i++ {
log.Entry().Infof(strings.Repeat("-", 25))
}
}
// AddDefaultDebugLine adds 25 dashes in debug
@@ -370,6 +379,7 @@ type ClientMock struct {
Error error
NilResponse bool
ErrorInsteadOfDump bool
ErrorList []error
}
// SetOptions sets clientOptions for a client mock
@@ -383,8 +393,10 @@ func (c *ClientMock) SendRequest(method, url string, bdy io.Reader, hdr http.Hea
}
var body []byte
var responseError error
if c.Body != "" {
body = []byte(c.Body)
responseError = c.Error
} else {
if c.ErrorInsteadOfDump && len(c.BodyList) == 0 {
return nil, errors.New("No more bodies in the list")
@@ -392,6 +404,12 @@ func (c *ClientMock) SendRequest(method, url string, bdy io.Reader, hdr http.Hea
bodyString := c.BodyList[len(c.BodyList)-1]
c.BodyList = c.BodyList[:len(c.BodyList)-1]
body = []byte(bodyString)
if len(c.ErrorList) == 0 {
responseError = c.Error
} else {
responseError = c.ErrorList[len(c.ErrorList)-1]
c.ErrorList = c.ErrorList[:len(c.ErrorList)-1]
}
}
header := http.Header{}
header.Set("X-Csrf-Token", c.Token)
@@ -399,7 +417,7 @@ func (c *ClientMock) SendRequest(method, url string, bdy io.Reader, hdr http.Hea
StatusCode: c.StatusCode,
Header: header,
Body: io.NopCloser(bytes.NewReader(body)),
}, c.Error
}, responseError
}
// DownloadFile : Empty file download

View File

@@ -309,7 +309,7 @@ func TestHandleHTTPError(t *testing.T) {
receivedErr := errors.New(errorValue)
message := "Custom Error Message"
err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{})
_, err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{})
assert.EqualError(t, err, fmt.Sprintf("%s: %s - %s", receivedErr.Error(), abapErrorCode, abapErrorMessage))
log.Entry().Info(err.Error())
})
@@ -328,7 +328,7 @@ func TestHandleHTTPError(t *testing.T) {
receivedErr := errors.New(errorValue)
message := "Custom Error Message"
err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{})
_, err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{})
assert.EqualError(t, err, fmt.Sprintf("%s", receivedErr.Error()))
log.Entry().Info(err.Error())
})
@@ -347,7 +347,7 @@ func TestHandleHTTPError(t *testing.T) {
receivedErr := errors.New(errorValue)
message := "Custom Error Message"
err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{})
_, err := HandleHTTPError(&resp, receivedErr, message, ConnectionDetailsHTTP{})
assert.EqualError(t, err, fmt.Sprintf("%s", receivedErr.Error()))
log.Entry().Info(err.Error())
})
@@ -361,7 +361,7 @@ func TestHandleHTTPError(t *testing.T) {
_, hook := test.NewNullLogger()
log.RegisterHook(hook)
err := HandleHTTPError(nil, receivedErr, message, ConnectionDetailsHTTP{})
_, err := HandleHTTPError(nil, receivedErr, message, ConnectionDetailsHTTP{})
assert.EqualError(t, err, fmt.Sprintf("%s", receivedErr.Error()))
assert.Equal(t, 5, len(hook.Entries), "Expected a different number of entries")

View File

@@ -1,51 +1,48 @@
package abaputils
import (
"encoding/json"
"fmt"
"io"
"reflect"
"sort"
"strconv"
"strings"
"time"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
)
const failureMessageClonePull = "Could not pull the Repository / Software Component "
const numberOfEntriesPerPage = 100000
const logOutputStatusLength = 10
const logOutputTimestampLength = 29
// PollEntity periodically polls the pull/import entity to get the status. Check if the import is still running
func PollEntity(repositoryName string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender, pollIntervall time.Duration) (string, error) {
// PollEntity periodically polls the action entity to get the status. Check if the import is still running
func PollEntity(api SoftwareComponentApiInterface, pollIntervall time.Duration) (string, error) {
log.Entry().Info("Start polling the status...")
var status string = "R"
var statusCode string = "R"
var err error
for {
pullEntity, responseStatus, err := GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client)
// pullEntity, responseStatus, err := api.GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client)
statusCode, err = api.GetAction()
if err != nil {
return status, err
return statusCode, err
}
status = pullEntity.Status
log.Entry().WithField("StatusCode", responseStatus).Info("Status: " + pullEntity.Status + " - " + pullEntity.StatusDescription)
if pullEntity.Status != "R" && pullEntity.Status != "Q" {
PrintLogs(repositoryName, connectionDetails, client)
if statusCode != "R" && statusCode != "Q" {
PrintLogs(api)
break
}
time.Sleep(pollIntervall)
}
return status, nil
return statusCode, nil
}
func PrintLogs(repositoryName string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) {
connectionDetails.URL = connectionDetails.URL + "?$expand=to_Log_Overview"
entity, _, err := GetStatus(failureMessageClonePull+repositoryName, connectionDetails, client)
func PrintLogs(api SoftwareComponentApiInterface) {
// connectionDetails.URL = connectionDetails.URL + "?$expand=to_Log_Overview"
entity, err := api.GetLogOverview()
if err != nil || len(entity.ToLogOverview.Results) == 0 {
// return if no logs are available
return
@@ -60,14 +57,14 @@ func PrintLogs(repositoryName string, connectionDetails ConnectionDetailsHTTP, c
// Print Details
for _, logEntryForDetails := range entity.ToLogOverview.Results {
printLog(logEntryForDetails, connectionDetails, client)
printLog(logEntryForDetails, api)
}
AddDefaultDashedLine()
AddDefaultDashedLine(1)
return
}
func printOverview(entity PullEntity) {
func printOverview(entity ActionEntity) {
logOutputPhaseLength, logOutputLineLength := calculateLenghts(entity)
@@ -85,7 +82,7 @@ func printOverview(entity PullEntity) {
printDashedLine(logOutputLineLength)
}
func calculateLenghts(entity PullEntity) (int, int) {
func calculateLenghts(entity ActionEntity) (int, int) {
phaseLength := 22
for _, logEntry := range entity.ToLogOverview.Results {
if l := len(logEntry.Name); l > phaseLength {
@@ -101,24 +98,18 @@ func printDashedLine(i int) {
log.Entry().Infof(strings.Repeat("-", i))
}
func printLog(logOverviewEntry LogResultsV2, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) {
func printLog(logOverviewEntry LogResultsV2, api SoftwareComponentApiInterface) {
page := 0
printHeader(logOverviewEntry)
for {
connectionDetails.URL = logOverviewEntry.ToLogProtocol.Deferred.URI + getLogProtocolQuery(page)
entity, err := GetProtocol(failureMessageClonePull, connectionDetails, client)
printLogProtocolEntries(logOverviewEntry, entity)
logProtocolEntry, err := api.GetLogProtocol(logOverviewEntry, page)
printLogProtocolEntries(logOverviewEntry, logProtocolEntry)
page += 1
if allLogsHaveBeenPrinted(entity, page, err) {
if allLogsHaveBeenPrinted(logProtocolEntry, page, err) {
break
}
}
}
func printLogProtocolEntries(logEntry LogResultsV2, entity LogProtocolResults) {
@@ -126,12 +117,10 @@ func printLogProtocolEntries(logEntry LogResultsV2, entity LogProtocolResults) {
sort.SliceStable(entity.Results, func(i, j int) bool {
return entity.Results[i].ProtocolLine < entity.Results[j].ProtocolLine
})
if logEntry.Status != `Success` {
for _, entry := range entity.Results {
log.Entry().Info(entry.Description)
}
} else {
for _, entry := range entity.Results {
log.Entry().Debug(entry.Description)
@@ -144,6 +133,8 @@ func allLogsHaveBeenPrinted(entity LogProtocolResults, page int, err error) bool
numberOfProtocols, errConversion := strconv.Atoi(entity.Count)
if errConversion == nil {
allPagesHaveBeenRead = numberOfProtocols <= page*numberOfEntriesPerPage
} else {
return true
}
return (err != nil || allPagesHaveBeenRead || reflect.DeepEqual(entity.Results, LogProtocolResults{}))
}
@@ -151,9 +142,9 @@ func allLogsHaveBeenPrinted(entity LogProtocolResults, page int, err error) bool
func printHeader(logEntry LogResultsV2) {
if logEntry.Status != `Success` {
log.Entry().Infof("\n")
AddDefaultDashedLine()
AddDefaultDashedLine(1)
log.Entry().Infof("%s (%v)", logEntry.Name, ConvertTime(logEntry.Timestamp))
AddDefaultDashedLine()
AddDefaultDashedLine(1)
} else {
log.Entry().Debugf("\n")
AddDebugDashedLine()
@@ -169,65 +160,6 @@ func getLogProtocolQuery(page int) string {
return fmt.Sprintf("?$skip=%s&$top=%s&$inlinecount=allpages", fmt.Sprint(skip), fmt.Sprint(top))
}
func GetStatus(failureMessage string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) (body PullEntity, status string, err error) {
resp, err := GetHTTPResponse("GET", connectionDetails, nil, client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
err = HandleHTTPError(resp, err, failureMessage, connectionDetails)
if resp != nil {
status = resp.Status
}
return body, status, err
}
defer resp.Body.Close()
// Parse response
var abapResp map[string]*json.RawMessage
bodyText, _ := io.ReadAll(resp.Body)
marshallError := json.Unmarshal(bodyText, &abapResp)
if marshallError != nil {
return body, status, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system")
}
marshallError = json.Unmarshal(*abapResp["d"], &body)
if marshallError != nil {
return body, status, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system")
}
if reflect.DeepEqual(PullEntity{}, body) {
log.Entry().WithField("StatusCode", resp.Status).Error(failureMessage)
log.SetErrorCategory(log.ErrorInfrastructure)
var err = errors.New("Request to ABAP System not successful")
return body, resp.Status, err
}
return body, resp.Status, nil
}
func GetProtocol(failureMessage string, connectionDetails ConnectionDetailsHTTP, client piperhttp.Sender) (body LogProtocolResults, err error) {
resp, err := GetHTTPResponse("GET", connectionDetails, nil, client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
err = HandleHTTPError(resp, err, failureMessage, connectionDetails)
return body, err
}
defer resp.Body.Close()
// Parse response
var abapResp map[string]*json.RawMessage
bodyText, _ := io.ReadAll(resp.Body)
marshallError := json.Unmarshal(bodyText, &abapResp)
if marshallError != nil {
return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system")
}
marshallError = json.Unmarshal(*abapResp["d"], &body)
if marshallError != nil {
return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system")
}
return body, nil
}
// GetRepositories for parsing one or multiple branches and repositories from repositories file or branchName and repositoryName configuration
func GetRepositories(config *RepositoriesConfig, branchRequired bool) ([]Repository, error) {
var repositories = make([]Repository, 0)
@@ -313,118 +245,3 @@ func (repo *Repository) GetPullLogString() (logString string) {
logString = "repository / software component '" + repo.Name + "'" + commitOrTag
return logString
}
/****************************************
* Structs for the A4C_A2G_GHA service *
****************************************/
// PullEntity struct for the Pull/Import entity A4C_A2G_GHA_SC_IMP
type PullEntity struct {
Metadata AbapMetadata `json:"__metadata"`
UUID string `json:"uuid"`
Namespace string `json:"namepsace"`
ScName string `json:"sc_name"`
ImportType string `json:"import_type"`
BranchName string `json:"branch_name"`
StartedByUser string `json:"user_name"`
Status string `json:"status"`
StatusDescription string `json:"status_descr"`
CommitID string `json:"commit_id"`
StartTime string `json:"start_time"`
ChangeTime string `json:"change_time"`
ToExecutionLog AbapLogs `json:"to_Execution_log"`
ToTransportLog AbapLogs `json:"to_Transport_log"`
ToLogOverview AbapLogsV2 `json:"to_Log_Overview"`
}
// BranchEntity struct for the Branch entity A4C_A2G_GHA_SC_BRANCH
type BranchEntity struct {
Metadata AbapMetadata `json:"__metadata"`
ScName string `json:"sc_name"`
Namespace string `json:"namepsace"`
BranchName string `json:"branch_name"`
ParentBranch string `json:"derived_from"`
CreatedBy string `json:"created_by"`
CreatedOn string `json:"created_on"`
IsActive bool `json:"is_active"`
CommitID string `json:"commit_id"`
CommitMessage string `json:"commit_message"`
LastCommitBy string `json:"last_commit_by"`
LastCommitOn string `json:"last_commit_on"`
}
// CloneEntity struct for the Clone entity A4C_A2G_GHA_SC_CLONE
type CloneEntity struct {
Metadata AbapMetadata `json:"__metadata"`
UUID string `json:"uuid"`
ScName string `json:"sc_name"`
BranchName string `json:"branch_name"`
ImportType string `json:"import_type"`
Namespace string `json:"namepsace"`
Status string `json:"status"`
StatusDescription string `json:"status_descr"`
StartedByUser string `json:"user_name"`
StartTime string `json:"start_time"`
ChangeTime string `json:"change_time"`
}
// AbapLogs struct for ABAP logs
type AbapLogs struct {
Results []LogResults `json:"results"`
}
type AbapLogsV2 struct {
Results []LogResultsV2 `json:"results"`
}
type LogResultsV2 struct {
Metadata AbapMetadata `json:"__metadata"`
Index int `json:"log_index"`
Name string `json:"log_name"`
Status string `json:"type_of_found_issues"`
Timestamp string `json:"timestamp"`
ToLogProtocol LogProtocolDeferred `json:"to_Log_Protocol"`
}
type LogProtocolDeferred struct {
Deferred URI `json:"__deferred"`
}
type URI struct {
URI string `json:"uri"`
}
type LogProtocolResults struct {
Results []LogProtocol `json:"results"`
Count string `json:"__count"`
}
type LogProtocol struct {
Metadata AbapMetadata `json:"__metadata"`
OverviewIndex int `json:"log_index"`
ProtocolLine int `json:"index_no"`
Type string `json:"type"`
Description string `json:"descr"`
Timestamp string `json:"timestamp"`
}
// LogResults struct for Execution and Transport Log entities A4C_A2G_GHA_SC_LOG_EXE and A4C_A2G_GHA_SC_LOG_TP
type LogResults struct {
Index string `json:"index_no"`
Type string `json:"type"`
Description string `json:"descr"`
Timestamp string `json:"timestamp"`
}
// RepositoriesConfig struct for parsing one or multiple branches and repositories configurations
type RepositoriesConfig struct {
BranchName string
CommitID string
RepositoryName string
RepositoryNames []string
Repositories string
}
type EntitySetsForManageGitRepository struct {
EntitySets []string `json:"EntitySets"`
}

View File

@@ -10,7 +10,6 @@ import (
"os"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
@@ -46,33 +45,25 @@ func TestPollEntity(t *testing.T) {
logResultSuccess,
`{"d" : { "status" : "S" } }`,
`{"d" : { "status" : "R" } }`,
`{"d" : { "status" : "Q" } }`,
`{}`,
},
Token: "myToken",
StatusCode: 200,
}
options := AbapEnvironmentOptions{
CfAPIEndpoint: "https://api.endpoint.com",
CfOrg: "testOrg",
CfSpace: "testSpace",
CfServiceInstance: "testInstance",
CfServiceKeyName: "testServiceKey",
Username: "testUser",
Password: "testPassword",
}
config := AbapEnvironmentCheckoutBranchOptions{
AbapEnvOptions: options,
RepositoryName: "testRepo1",
}
con := ConnectionDetailsHTTP{
User: "MY_USER",
Password: "MY_PW",
URL: "https://api.endpoint.com/Entity/",
XCsrfToken: "MY_TOKEN",
}
status, _ := PollEntity(config.RepositoryName, con, client, 0)
swcManager := SoftwareComponentApiManager{Client: client}
repo := Repository{Name: "testRepo1"}
api, _ := swcManager.GetAPI(con, repo)
status, _ := PollEntity(api, 0)
assert.Equal(t, "S", status)
assert.Equal(t, 0, len(client.BodyList), "Not all requests were done")
})
@@ -87,33 +78,24 @@ func TestPollEntity(t *testing.T) {
`{"d" : { "status" : "E" } }`,
`{"d" : { "status" : "R" } }`,
`{"d" : { "status" : "Q" } }`,
`{}`,
},
Token: "myToken",
StatusCode: 200,
}
options := AbapEnvironmentOptions{
CfAPIEndpoint: "https://api.endpoint.com",
CfOrg: "testOrg",
CfSpace: "testSpace",
CfServiceInstance: "testInstance",
CfServiceKeyName: "testServiceKey",
Username: "testUser",
Password: "testPassword",
}
config := AbapEnvironmentCheckoutBranchOptions{
AbapEnvOptions: options,
RepositoryName: "testRepo1",
}
con := ConnectionDetailsHTTP{
User: "MY_USER",
Password: "MY_PW",
URL: "https://api.endpoint.com/Entity/",
XCsrfToken: "MY_TOKEN",
}
status, _ := PollEntity(config.RepositoryName, con, client, 0)
swcManager := SoftwareComponentApiManager{Client: client}
repo := Repository{Name: "testRepo1"}
api, _ := swcManager.GetAPI(con, repo)
status, _ := PollEntity(api, 0)
assert.Equal(t, "E", status)
assert.Equal(t, 0, len(client.BodyList), "Not all requests were done")
})
@@ -318,22 +300,3 @@ func TestCreateRequestBodies(t *testing.T) {
assert.Equal(t, `{"sc_name":"/DMO/REPO", "tag_name":"myTag"}`, body, "Expected different body")
})
}
func TestGetStatus(t *testing.T) {
t.Run("Graceful Exit", func(t *testing.T) {
client := &ClientMock{
NilResponse: true,
Error: errors.New("Backend Error"),
StatusCode: 500,
}
connectionDetails := ConnectionDetailsHTTP{
URL: "example.com",
}
_, status, err := GetStatus("failure message", connectionDetails, client)
assert.Error(t, err, "Expected Error")
assert.Equal(t, "", status)
})
}

View File

@@ -0,0 +1,369 @@
package abaputils
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"reflect"
"strings"
"time"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
"k8s.io/utils/strings/slices"
)
type SAP_COM_0510 struct {
con ConnectionDetailsHTTP
client piperhttp.Sender
repository Repository
path string
cloneEntity string
repositoryEntity string
tagsEntity string
checkoutAction string
actionEntity string
uuid string
failureMessage string
maxRetries int
retryBaseSleepUnit time.Duration
retryMaxSleepTime time.Duration
retryAllowedErrorCodes []string
}
func (api *SAP_COM_0510) init(con ConnectionDetailsHTTP, client piperhttp.Sender, repo Repository) {
api.con = con
api.client = client
api.repository = repo
api.path = "/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY"
api.cloneEntity = "/Clones"
api.repositoryEntity = "/Repositories"
api.tagsEntity = "/Tags"
api.actionEntity = "/Pull"
api.checkoutAction = "/checkout_branch"
api.failureMessage = "The action of the Repository / Software Component " + api.repository.Name + " failed"
api.maxRetries = 3
api.setSleepTimeConfig(1*time.Second, 120*time.Second)
api.retryAllowedErrorCodes = append(api.retryAllowedErrorCodes, "A4C_A2G/228")
}
func (api *SAP_COM_0510) getUUID() string {
return api.uuid
}
func (api *SAP_COM_0510) CreateTag(tag Tag) error {
if reflect.DeepEqual(Tag{}, tag) {
return errors.New("No Tag provided")
}
con := api.con
con.URL = api.con.URL + api.path + api.tagsEntity
requestBodyStruct := CreateTagBody{RepositoryName: api.repository.Name, CommitID: api.repository.CommitID, Tag: tag.TagName, Description: tag.TagDescription}
jsonBody, err := json.Marshal(&requestBodyStruct)
if err != nil {
return err
}
return api.triggerRequest(con, jsonBody)
}
func (api *SAP_COM_0510) CheckoutBranch() error {
if api.repository.Name == "" || api.repository.Branch == "" {
return fmt.Errorf("Failed to trigger checkout: %w", errors.New("Repository and/or Branch Configuration is empty. Please make sure that you have specified the correct values"))
}
// the request looks like: POST/sap/opu/odata/sap/MANAGE_GIT_REPOSITORY/checkout_branch?branch_name='newBranch'&sc_name=/DMO/GIT_REPOSITORY'
checkoutConnectionDetails := api.con
checkoutConnectionDetails.URL = api.con.URL + api.path + api.checkoutAction + `?branch_name='` + api.repository.Branch + `'&sc_name='` + api.repository.Name + `'`
jsonBody := []byte(``)
return api.triggerRequest(checkoutConnectionDetails, jsonBody)
}
func (api *SAP_COM_0510) parseActionResponse(resp *http.Response, err error) (ActionEntity, error) {
var body ActionEntity
var abapResp map[string]*json.RawMessage
bodyText, errRead := io.ReadAll(resp.Body)
if errRead != nil {
return ActionEntity{}, err
}
if err := json.Unmarshal(bodyText, &abapResp); err != nil {
return ActionEntity{}, err
}
if err := json.Unmarshal(*abapResp["d"], &body); err != nil {
return ActionEntity{}, err
}
if reflect.DeepEqual(ActionEntity{}, body) {
log.Entry().WithField("StatusCode", resp.Status).WithField("branchName", api.repository.Branch).Error("Could not switch to specified branch")
err := errors.New("Request to ABAP System not successful")
return ActionEntity{}, err
}
return body, nil
}
func (api *SAP_COM_0510) Pull() error {
// Trigger the Pull of a Repository
if api.repository.Name == "" {
return errors.New("An empty string was passed for the parameter 'repositoryName'")
}
pullConnectionDetails := api.con
pullConnectionDetails.URL = api.con.URL + api.path + api.actionEntity
jsonBody := []byte(api.repository.GetPullRequestBody())
return api.triggerRequest(pullConnectionDetails, jsonBody)
}
func (api *SAP_COM_0510) GetLogProtocol(logOverviewEntry LogResultsV2, page int) (body LogProtocolResults, err error) {
connectionDetails := api.con
connectionDetails.URL = logOverviewEntry.ToLogProtocol.Deferred.URI + getLogProtocolQuery(page)
resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
return body, err
}
defer resp.Body.Close()
// Parse response
var abapResp map[string]*json.RawMessage
bodyText, _ := io.ReadAll(resp.Body)
marshallError := json.Unmarshal(bodyText, &abapResp)
if marshallError != nil {
return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system")
}
marshallError = json.Unmarshal(*abapResp["d"], &body)
if marshallError != nil {
return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system")
}
return body, nil
}
func (api *SAP_COM_0510) GetLogOverview() (body ActionEntity, err error) {
connectionDetails := api.con
connectionDetails.URL = api.con.URL + api.path + api.actionEntity + "(uuid=guid'" + api.getUUID() + "')" + "?$expand=to_Log_Overview"
resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
return body, err
}
defer resp.Body.Close()
// Parse response
var abapResp map[string]*json.RawMessage
bodyText, _ := io.ReadAll(resp.Body)
marshallError := json.Unmarshal(bodyText, &abapResp)
if marshallError != nil {
return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system")
}
marshallError = json.Unmarshal(*abapResp["d"], &body)
if marshallError != nil {
return body, errors.Wrap(marshallError, "Could not parse response from the ABAP Environment system")
}
if reflect.DeepEqual(ActionEntity{}, body) {
log.Entry().WithField("StatusCode", resp.Status).Error(api.failureMessage)
log.SetErrorCategory(log.ErrorInfrastructure)
var err = errors.New("Request to ABAP System not successful")
return body, err
}
abapStatusCode := body.Status
log.Entry().Info("Status: " + abapStatusCode + " - " + body.StatusDescription)
return body, nil
}
func (api *SAP_COM_0510) GetAction() (string, error) {
connectionDetails := api.con
connectionDetails.URL = api.con.URL + api.path + api.actionEntity + "(uuid=guid'" + api.getUUID() + "')"
resp, err := GetHTTPResponse("GET", connectionDetails, nil, api.client)
if err != nil {
log.SetErrorCategory(log.ErrorInfrastructure)
_, err = HandleHTTPError(resp, err, api.failureMessage, connectionDetails)
return "E", err
}
defer resp.Body.Close()
// Parse Response
body, parseError := api.parseActionResponse(resp, err)
if parseError != nil {
return "E", parseError
}
api.uuid = body.UUID
abapStatusCode := body.Status
log.Entry().Info("Status: " + abapStatusCode + " - " + body.StatusDescription)
return abapStatusCode, nil
}
func (api *SAP_COM_0510) GetRepository() (bool, string, error) {
if api.repository.Name == "" {
return false, "", errors.New("An empty string was passed for the parameter 'repositoryName'")
}
swcConnectionDetails := api.con
swcConnectionDetails.URL = api.con.URL + api.path + api.repositoryEntity + "('" + strings.Replace(api.repository.Name, "/", "%2F", -1) + "')"
resp, err := GetHTTPResponse("GET", swcConnectionDetails, nil, api.client)
if err != nil {
_, errRepo := HandleHTTPError(resp, err, "Reading the Repository / Software Component failed", api.con)
return false, "", errRepo
}
defer resp.Body.Close()
var body RepositoryEntity
var abapResp map[string]*json.RawMessage
bodyText, errRead := io.ReadAll(resp.Body)
if errRead != nil {
return false, "", err
}
if err := json.Unmarshal(bodyText, &abapResp); err != nil {
return false, "", err
}
if err := json.Unmarshal(*abapResp["d"], &body); err != nil {
return false, "", err
}
if reflect.DeepEqual(RepositoryEntity{}, body) {
log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", api.repository.Name).WithField("branchName", api.repository.Branch).WithField("commitID", api.repository.CommitID).WithField("Tag", api.repository.Tag).Error("Could not Clone the Repository / Software Component")
err := errors.New("Request to ABAP System not successful")
return false, "", err
}
if body.AvailOnInst {
return true, body.ActiveBranch, nil
}
return false, "", err
}
func (api *SAP_COM_0510) Clone() error {
// Trigger the Clone of a Repository
if api.repository.Name == "" {
return errors.New("An empty string was passed for the parameter 'repositoryName'")
}
cloneConnectionDetails := api.con
cloneConnectionDetails.URL = api.con.URL + api.path + api.cloneEntity
body := []byte(api.repository.GetCloneRequestBody())
return api.triggerRequest(cloneConnectionDetails, body)
}
func (api *SAP_COM_0510) triggerRequest(cloneConnectionDetails ConnectionDetailsHTTP, jsonBody []byte) error {
var err error
var body ActionEntity
var resp *http.Response
var errorCode string
for i := 0; i <= api.maxRetries; i++ {
if i > 0 {
sleepTime, err := api.getSleepTime(i + 5)
if err != nil {
// reached max retry duration
break
}
log.Entry().Infof("Retrying in %s", sleepTime.String())
time.Sleep(sleepTime)
}
resp, err = GetHTTPResponse("POST", cloneConnectionDetails, jsonBody, api.client)
if err != nil {
errorCode, err = HandleHTTPError(resp, err, "Triggering the action failed", api.con)
if slices.Contains(api.retryAllowedErrorCodes, errorCode) {
// Error Code allows for retry
continue
} else {
break
}
}
defer resp.Body.Close()
log.Entry().WithField("StatusCode", resp.Status).WithField("repositoryName", api.repository.Name).WithField("branchName", api.repository.Branch).WithField("commitID", api.repository.CommitID).WithField("Tag", api.repository.Tag).Info("Triggered action of Repository / Software Component")
body, err = api.parseActionResponse(resp, err)
break
}
api.uuid = body.UUID
return err
}
// initialRequest implements SoftwareComponentApiInterface.
func (api *SAP_COM_0510) initialRequest() error {
// Configuring the HTTP Client and CookieJar
cookieJar, errorCookieJar := cookiejar.New(nil)
if errorCookieJar != nil {
return errors.Wrap(errorCookieJar, "Could not create a Cookie Jar")
}
api.client.SetOptions(piperhttp.ClientOptions{
MaxRequestDuration: 180 * time.Second,
CookieJar: cookieJar,
Username: api.con.User,
Password: api.con.Password,
})
headConnection := api.con
headConnection.XCsrfToken = "fetch"
headConnection.URL = api.con.URL + api.path
// Loging into the ABAP System - getting the x-csrf-token and cookies
resp, err := GetHTTPResponse("HEAD", headConnection, nil, api.client)
if err != nil {
_, err = HandleHTTPError(resp, err, "Authentication on the ABAP system failed", api.con)
return err
}
defer resp.Body.Close()
log.Entry().WithField("StatusCode", resp.Status).WithField("ABAP Endpoint", api.con).Debug("Authentication on the ABAP system successful")
api.con.XCsrfToken = resp.Header.Get("X-Csrf-Token")
return nil
}
// getSleepTime Should return the Fibonacci numbers in the define time unit up to the defined maximum duration
func (api *SAP_COM_0510) getSleepTime(n int) (time.Duration, error) {
if n == 0 {
return 0, nil
} else if n == 1 {
return 1 * api.retryBaseSleepUnit, nil
} else if n < 0 {
return 0, errors.New("Negative numbers are not allowed")
}
var result, i int
prev := 0
next := 1
for i = 2; i <= n; i++ {
result = prev + next
prev = next
next = result
}
sleepTime := time.Duration(result) * api.retryBaseSleepUnit
if sleepTime > api.retryMaxSleepTime {
return 0, errors.New("Exceeded max sleep time")
}
return sleepTime, nil
}
// setSleepTimeConfig sets the time unit (seconds, nanoseconds) and the maximum sleep duration
func (api *SAP_COM_0510) setSleepTimeConfig(timeUnit time.Duration, maxSleepTime time.Duration) {
api.retryBaseSleepUnit = timeUnit
api.retryMaxSleepTime = maxSleepTime
}

View File

@@ -0,0 +1,483 @@
//go:build unit
// +build unit
package abaputils
import (
"testing"
"time"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
var con ConnectionDetailsHTTP
var repo Repository
func init() {
con.User = "CC_USER"
con.Password = "123abc"
con.URL = "https://example.com"
repo.Name = "/DMO/REPO"
repo.Branch = "main"
}
func TestRetry(t *testing.T) {
t.Run("Test retry success", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"d" : { "status" : "R", "UUID" : "GUID" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Software component lifecycle activities in progress. Try again later..."} } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
ErrorList: []error{
nil,
errors.New("HTTP 400"),
nil,
},
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errAction := api.(*SAP_COM_0510).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}"))
assert.NoError(t, errAction)
assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID")
})
t.Run("Test retry not allowed", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"d" : { "status" : "R", "UUID" : "GUID" } }`,
`{"error" : { "code" : "A4C_A2G/224", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
ErrorList: []error{
nil,
errors.New("HTTP 400"),
nil,
},
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errAction := api.(*SAP_COM_0510).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}"))
assert.ErrorContains(t, errAction, "HTTP 400: A4C_A2G/224 - Error Text")
assert.Empty(t, api.getUUID(), "API does not cotain correct UUID")
})
t.Run("Test retry maxSleepTime", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
ErrorList: []error{
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
nil,
},
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
api.setSleepTimeConfig(time.Nanosecond, 20*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
api.(*SAP_COM_0510).maxRetries = 20
errAction := api.(*SAP_COM_0510).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}"))
assert.ErrorContains(t, errAction, "HTTP 400: A4C_A2G/228 - Error Text")
assert.Empty(t, api.getUUID(), "API does not cotain correct UUID")
assert.Equal(t, 6, len(client.BodyList), "Expected maxSleepTime to limit requests")
})
t.Run("Test retry maxRetries", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Error Text"} } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
ErrorList: []error{
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
errors.New("HTTP 400"),
nil,
},
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
api.setSleepTimeConfig(time.Nanosecond, 999*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
api.(*SAP_COM_0510).maxRetries = 3
errAction := api.(*SAP_COM_0510).triggerRequest(ConnectionDetailsHTTP{User: "CC_USER", Password: "abc123", URL: "https://example.com/path"}, []byte("{}"))
assert.ErrorContains(t, errAction, "HTTP 400: A4C_A2G/228 - Error Text")
assert.Empty(t, api.getUUID(), "API does not cotain correct UUID")
assert.Equal(t, 5, len(client.BodyList), "Expected maxRetries to limit requests")
})
}
func TestClone(t *testing.T) {
t.Run("Test Clone Success", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"d" : { "status" : "R", "UUID" : "GUID" } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errClone := api.Clone()
assert.NoError(t, errClone)
assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID")
})
t.Run("Test Clone Failure", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{ "d" : {} }`,
`{ "d" : {} }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errClone := api.Clone()
assert.ErrorContains(t, errClone, "Request to ABAP System not successful")
assert.Empty(t, api.getUUID(), "API does not cotain correct UUID")
})
t.Run("Test Clone Retry", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"d" : { "status" : "R", "UUID" : "GUID" } }`,
`{"error" : { "code" : "A4C_A2G/228", "message" : { "lang" : "de", "value" : "Software component lifecycle activities in progress. Try again later..."} } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
ErrorList: []error{
nil,
errors.New("HTTP 400"),
nil,
},
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
api.setSleepTimeConfig(time.Nanosecond, 120*time.Nanosecond)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errClone := api.Clone()
assert.NoError(t, errClone)
assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID")
})
}
func TestPull(t *testing.T) {
t.Run("Test Pull Success", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"d" : { "status" : "R", "UUID" : "GUID" } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errPull := api.Pull()
assert.NoError(t, errPull)
assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID")
})
t.Run("Test Pull Failure", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{ "d" : {} }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errPull := api.Pull()
assert.ErrorContains(t, errPull, "Request to ABAP System not successful")
assert.Empty(t, api.getUUID(), "API does not cotain correct UUID")
})
}
func TestCheckout(t *testing.T) {
t.Run("Test Checkout Success", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"d" : { "status" : "R", "UUID" : "GUID" } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errCheckout := api.CheckoutBranch()
assert.NoError(t, errCheckout)
assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID")
})
t.Run("Test Checkout Failure", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{ "d" : {} }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errCheckoput := api.CheckoutBranch()
assert.ErrorContains(t, errCheckoput, "Request to ABAP System not successful")
assert.Empty(t, api.getUUID(), "API does not cotain correct UUID")
})
}
func TestGetRepo(t *testing.T) {
t.Run("Test GetRepo Success", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"d" : { "sc_name" : "testRepo1", "avail_on_inst" : true, "active_branch": "testBranch1" } }`,
`{"d" : [] }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
cloned, activeBranch, errAction := api.GetRepository()
assert.True(t, cloned)
assert.Equal(t, "testBranch1", activeBranch)
assert.NoError(t, errAction)
})
}
func TestCreateTag(t *testing.T) {
t.Run("Test Tag Success", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{"d" : { "status" : "R", "UUID" : "GUID" } }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errCreateTag := api.CreateTag(Tag{TagName: "myTag", TagDescription: "descr"})
assert.NoError(t, errCreateTag)
assert.Equal(t, "GUID", api.getUUID(), "API does not cotain correct UUID")
})
t.Run("Test Tag Failure", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{ "d" : {} }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errCreateTag := api.CreateTag(Tag{TagName: "myTag", TagDescription: "descr"})
assert.ErrorContains(t, errCreateTag, "Request to ABAP System not successful")
assert.Empty(t, api.getUUID(), "API does not cotain correct UUID")
})
t.Run("Test Tag Empty", func(t *testing.T) {
client := &ClientMock{
BodyList: []string{
`{ "d" : {} }`,
`{ }`,
},
Token: "myToken",
StatusCode: 200,
}
apiManager := &SoftwareComponentApiManager{Client: client, PollIntervall: 1 * time.Microsecond}
api, err := apiManager.GetAPI(con, repo)
assert.NoError(t, err)
assert.IsType(t, &SAP_COM_0510{}, api.(*SAP_COM_0510), "API has wrong type")
errCreateTag := api.CreateTag(Tag{})
assert.ErrorContains(t, errCreateTag, "No Tag provided")
assert.Empty(t, api.getUUID(), "API does not cotain correct UUID")
})
}
func TestSleepTime(t *testing.T) {
t.Run("Test Sleep Time", func(t *testing.T) {
api := SAP_COM_0510{
retryMaxSleepTime: 120 * time.Nanosecond,
retryBaseSleepUnit: 1 * time.Nanosecond,
}
expectedResults := make([]time.Duration, 12)
expectedResults[0] = 0
expectedResults[1] = 1
expectedResults[2] = 1
expectedResults[3] = 2
expectedResults[4] = 3
expectedResults[5] = 5
expectedResults[6] = 8
expectedResults[7] = 13
expectedResults[8] = 21
expectedResults[9] = 34
expectedResults[10] = 55
expectedResults[11] = 89
results := make([]time.Duration, 12)
var err error
for i := 0; i <= 11; i++ {
results[i], err = api.getSleepTime(i)
assert.NoError(t, err)
}
assert.ElementsMatch(t, expectedResults, results)
_, err = api.getSleepTime(-10)
assert.Error(t, err)
_, err = api.getSleepTime(12)
assert.ErrorContains(t, err, "Exceeded max sleep time")
})
}

View File

@@ -0,0 +1,194 @@
package abaputils
import (
"time"
piperhttp "github.com/SAP/jenkins-library/pkg/http"
)
type SoftwareComponentApiManagerInterface interface {
GetAPI(con ConnectionDetailsHTTP, repo Repository) (SoftwareComponentApiInterface, error)
GetPollIntervall() time.Duration
}
type SoftwareComponentApiManager struct {
Client piperhttp.Sender
PollIntervall time.Duration
}
func (manager *SoftwareComponentApiManager) GetAPI(con ConnectionDetailsHTTP, repo Repository) (SoftwareComponentApiInterface, error) {
sap_com_0510 := SAP_COM_0510{}
sap_com_0510.init(con, manager.Client, repo)
// Initialize all APIs, use the one that returns a response
// Currently SAP_COM_0510, later SAP_COM_0948
err := sap_com_0510.initialRequest()
return &sap_com_0510, err
}
func (manager *SoftwareComponentApiManager) GetPollIntervall() time.Duration {
if manager.PollIntervall == 0 {
manager.PollIntervall = 5 * time.Second
}
return manager.PollIntervall
}
type SoftwareComponentApiInterface interface {
init(con ConnectionDetailsHTTP, client piperhttp.Sender, repo Repository)
initialRequest() error
setSleepTimeConfig(timeUnit time.Duration, maxSleepTime time.Duration)
getSleepTime(n int) (time.Duration, error)
getUUID() string
Clone() error
Pull() error
CheckoutBranch() error
GetRepository() (bool, string, error)
GetAction() (string, error)
GetLogOverview() (ActionEntity, error)
GetLogProtocol(LogResultsV2, int) (body LogProtocolResults, err error)
CreateTag(tag Tag) error
}
/****************************************
* Structs for the A4C_A2G_GHA service *
****************************************/
// ActionEntity struct for the Pull/Import entity A4C_A2G_GHA_SC_IMP
type ActionEntity struct {
Metadata AbapMetadata `json:"__metadata"`
UUID string `json:"uuid"`
Namespace string `json:"namepsace"`
ScName string `json:"sc_name"`
ImportType string `json:"import_type"`
BranchName string `json:"branch_name"`
StartedByUser string `json:"user_name"`
Status string `json:"status"`
StatusDescription string `json:"status_descr"`
CommitID string `json:"commit_id"`
StartTime string `json:"start_time"`
ChangeTime string `json:"change_time"`
ToExecutionLog AbapLogs `json:"to_Execution_log"`
ToTransportLog AbapLogs `json:"to_Transport_log"`
ToLogOverview AbapLogsV2 `json:"to_Log_Overview"`
}
// BranchEntity struct for the Branch entity A4C_A2G_GHA_SC_BRANCH
type BranchEntity struct {
Metadata AbapMetadata `json:"__metadata"`
ScName string `json:"sc_name"`
Namespace string `json:"namepsace"`
BranchName string `json:"branch_name"`
ParentBranch string `json:"derived_from"`
CreatedBy string `json:"created_by"`
CreatedOn string `json:"created_on"`
IsActive bool `json:"is_active"`
CommitID string `json:"commit_id"`
CommitMessage string `json:"commit_message"`
LastCommitBy string `json:"last_commit_by"`
LastCommitOn string `json:"last_commit_on"`
}
// CloneEntity struct for the Clone entity A4C_A2G_GHA_SC_CLONE
type CloneEntity struct {
Metadata AbapMetadata `json:"__metadata"`
UUID string `json:"uuid"`
ScName string `json:"sc_name"`
BranchName string `json:"branch_name"`
ImportType string `json:"import_type"`
Namespace string `json:"namepsace"`
Status string `json:"status"`
StatusDescription string `json:"status_descr"`
StartedByUser string `json:"user_name"`
StartTime string `json:"start_time"`
ChangeTime string `json:"change_time"`
}
type RepositoryEntity struct {
Metadata AbapMetadata `json:"__metadata"`
ScName string `json:"sc_name"`
ActiveBranch string `json:"active_branch"`
AvailOnInst bool `json:"avail_on_inst"`
}
// AbapLogs struct for ABAP logs
type AbapLogs struct {
Results []LogResults `json:"results"`
}
type AbapLogsV2 struct {
Results []LogResultsV2 `json:"results"`
}
type LogResultsV2 struct {
Metadata AbapMetadata `json:"__metadata"`
Index int `json:"log_index"`
Name string `json:"log_name"`
Status string `json:"type_of_found_issues"`
Timestamp string `json:"timestamp"`
ToLogProtocol LogProtocolDeferred `json:"to_Log_Protocol"`
}
type LogProtocolDeferred struct {
Deferred URI `json:"__deferred"`
}
type URI struct {
URI string `json:"uri"`
}
type LogProtocolResults struct {
Results []LogProtocol `json:"results"`
Count string `json:"__count"`
}
type LogProtocol struct {
Metadata AbapMetadata `json:"__metadata"`
OverviewIndex int `json:"log_index"`
ProtocolLine int `json:"index_no"`
Type string `json:"type"`
Description string `json:"descr"`
Timestamp string `json:"timestamp"`
}
// LogResults struct for Execution and Transport Log entities A4C_A2G_GHA_SC_LOG_EXE and A4C_A2G_GHA_SC_LOG_TP
type LogResults struct {
Index string `json:"index_no"`
Type string `json:"type"`
Description string `json:"descr"`
Timestamp string `json:"timestamp"`
}
// RepositoriesConfig struct for parsing one or multiple branches and repositories configurations
type RepositoriesConfig struct {
BranchName string
CommitID string
RepositoryName string
RepositoryNames []string
Repositories string
}
type EntitySetsForManageGitRepository struct {
EntitySets []string `json:"EntitySets"`
}
type CreateTagBacklog struct {
RepositoryName string
CommitID string
Tags []Tag
}
type Tag struct {
TagName string
TagDescription string
}
type CreateTagBody struct {
RepositoryName string `json:"sc_name"`
CommitID string `json:"commit_id"`
Tag string `json:"tag_name"`
Description string `json:"tag_description"`
}
type CreateTagResponse struct {
UUID string `json:"uuid"`
}