1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2025-01-18 05:18:24 +02:00

Merge pull request #1096 from SAP/http_uploader

Add upload of files to HTTP capabilities
This commit is contained in:
Sven Merk 2020-01-14 10:44:30 +01:00 committed by GitHub
commit 997ae27ccd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 206 additions and 27 deletions

View File

@ -1,13 +1,18 @@
package http
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strings"
"time"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// Client defines an http client object
@ -16,6 +21,7 @@ type Client struct {
username string
password string
token string
logger *logrus.Entry
}
// ClientOptions defines the options to be set on the client
@ -26,33 +32,112 @@ type ClientOptions struct {
Token string
}
// Sender provides an interface to the piper http client for uid/pwd authenticated requests
// Sender provides an interface to the piper http client for uid/pwd and token authenticated requests
type Sender interface {
SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error)
SetOptions(options ClientOptions)
}
// Uploader provides an interface to the piper http client for uid/pwd and token authenticated requests with upload capabilities
type Uploader interface {
SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error)
UploadFile(url, file, fieldName string, header http.Header, cookies []*http.Cookie) (*http.Response, error)
SetOptions(options ClientOptions)
}
// UploadFile uploads a file's content as multipart-form POST request to the specified URL
func (c *Client) UploadFile(url, file, fieldName string, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
httpClient := c.initialize()
bodyBuffer := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuffer)
fileWriter, err := bodyWriter.CreateFormFile(fieldName, file)
if err != nil {
return &http.Response{}, errors.Wrapf(err, "error creating form file %v for field %v", file, fieldName)
}
fileHandle, err := os.Open(file)
if err != nil {
return &http.Response{}, errors.Wrapf(err, "unable to locate file %v", file)
}
defer fileHandle.Close()
_, err = io.Copy(fileWriter, fileHandle)
if err != nil {
return &http.Response{}, errors.Wrapf(err, "unable to copy file content of %v into request body", file)
}
err = bodyWriter.Close()
method := http.MethodPost
request, err := c.createRequest(method, url, bodyBuffer, &header, cookies)
if err != nil {
c.logger.Debugf("New %v request to %v", method, url)
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v", method, url)
}
startBoundary := strings.Index(bodyWriter.FormDataContentType(), "=") + 1
boundary := bodyWriter.FormDataContentType()[startBoundary:]
request.Header.Add("Content-Type", "multipart/form-data; boundary=\""+boundary+"\"")
request.Header.Add("Connection", "Keep-Alive")
response, err := httpClient.Do(request)
if err != nil {
return response, errors.Wrapf(err, "HTTP %v request to %v failed with error", method, url)
}
return c.handleResponse(response)
}
// SendRequest sends an http request with a defined method
func (c *Client) SendRequest(method, url string, body io.Reader, header http.Header, cookies []*http.Cookie) (*http.Response, error) {
httpClient := c.initialize()
logger := log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
request, err := c.createRequest(method, url, body, &header, cookies)
if err != nil {
c.logger.Debugf("New %v request to %v", method, url)
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v", method, url)
}
response, err := httpClient.Do(request)
if err != nil {
return response, errors.Wrapf(err, "error opening %v", url)
}
return c.handleResponse(response)
}
// SetOptions sets options used for the http client
func (c *Client) SetOptions(options ClientOptions) {
c.timeout = options.Timeout
c.username = options.Username
c.password = options.Password
c.token = options.Token
c.logger = log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")
}
func (c *Client) initialize() *http.Client {
c.applyDefaults()
var httpClient = &http.Client{
Timeout: c.timeout,
}
logger.Debugf("Timeout set to %v", c.timeout)
c.logger.Debugf("Timeout set to %v", c.timeout)
return httpClient
}
func (c *Client) createRequest(method, url string, body io.Reader, header *http.Header, cookies []*http.Cookie) (*http.Request, error) {
c.logger.Debugf("New %v request to %v", method, url)
request, err := http.NewRequest(method, url, body)
logger.Debugf("New %v request to %v", method, url)
if err != nil {
return &http.Response{}, errors.Wrapf(err, "error creating %v request to %v", method, url)
return &http.Request{}, err
}
if header != nil {
for name, headers := range header {
for name, headers := range *header {
for _, h := range headers {
request.Header.Add(name, h)
}
@ -67,14 +152,17 @@ func (c *Client) SendRequest(method, url string, body io.Reader, header http.Hea
if len(c.username) > 0 && len(c.password) > 0 {
request.SetBasicAuth(c.username, c.password)
logger.Debug("Using Basic Authentication ****/****")
c.logger.Debug("Using Basic Authentication ****/****")
}
response, err := httpClient.Do(request)
if err != nil {
return response, errors.Wrapf(err, "error opening %v", url)
if len(c.token) > 0 {
request.Header.Add("Authorization", c.token)
}
return request, nil
}
func (c *Client) handleResponse(response *http.Response) (*http.Response, error) {
// 2xx codes do not create an error
if response.StatusCode >= 200 && response.StatusCode < 300 {
return response, nil
@ -82,24 +170,16 @@ func (c *Client) SendRequest(method, url string, body io.Reader, header http.Hea
switch response.StatusCode {
case http.StatusUnauthorized:
logger.WithField("HTTP Error", "401 (Unauthorized)").Error("Credentials invalid, please check your user credentials!")
c.logger.WithField("HTTP Error", "401 (Unauthorized)").Error("Credentials invalid, please check your user credentials!")
case http.StatusForbidden:
logger.WithField("HTTP Error", "403 (Forbidden)").Error("Permission issue, please check your user permissions!")
c.logger.WithField("HTTP Error", "403 (Forbidden)").Error("Permission issue, please check your user permissions!")
case http.StatusNotFound:
logger.WithField("HTTP Error", "404 (Not Found)").Error("Requested resource could not be found")
c.logger.WithField("HTTP Error", "404 (Not Found)").Error("Requested resource could not be found")
case http.StatusInternalServerError:
logger.WithField("HTTP Error", "500 (Internal Server Error)").Error("Unknown error occured.")
c.logger.WithField("HTTP Error", "500 (Internal Server Error)").Error("Unknown error occured.")
}
return response, fmt.Errorf("Request to %v returned with HTTP Code %v", url, response.StatusCode)
}
// SetOptions sets options used for the http client
func (c *Client) SetOptions(options ClientOptions) {
c.timeout = options.Timeout
c.username = options.Username
c.password = options.Password
c.token = options.Token
return response, fmt.Errorf("Request to %v returned with HTTP Code %v", response.Request.URL, response.StatusCode)
}
func (c *Client) applyDefaults() {

View File

@ -4,11 +4,14 @@ import (
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/SAP/jenkins-library/pkg/log"
"github.com/stretchr/testify/assert"
)
@ -41,10 +44,10 @@ func TestSendRequest(t *testing.T) {
cookies []*http.Cookie
expected string
}{
{client: Client{}, method: "GET", expected: "OK"},
{client: Client{}, method: "GET", header: map[string][]string{"Testheader": []string{"Test1", "Test2"}}, expected: "OK"},
{client: Client{}, cookies: []*http.Cookie{{Name: "TestCookie1", Value: "TestValue1"}, {Name: "TestCookie2", Value: "TestValue2"}}, method: "GET", expected: "OK"},
{client: Client{username: "TestUser", password: "TestPwd"}, method: "GET", expected: "OK"},
{client: Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}, method: "GET", expected: "OK"},
{client: Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}, method: "GET", header: map[string][]string{"Testheader": []string{"Test1", "Test2"}}, expected: "OK"},
{client: Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}, cookies: []*http.Cookie{{Name: "TestCookie1", Value: "TestValue1"}, {Name: "TestCookie2", Value: "TestValue2"}}, method: "GET", expected: "OK"},
{client: Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http"), username: "TestUser", password: "TestPwd"}, method: "GET", expected: "OK"},
}
for key, test := range tt {
@ -100,3 +103,99 @@ func TestApplyDefaults(t *testing.T) {
assert.Equal(t, v.expected, v.client, fmt.Sprintf("Run %v failed", k))
}
}
func TestUploadFile(t *testing.T) {
var passedHeaders = map[string][]string{}
passedCookies := []*http.Cookie{}
var passedUsername string
var passedPassword string
var multipartFile multipart.File
var multipartHeader *multipart.FileHeader
var passedFileContents []byte
// Start a local HTTP server
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
passedHeaders = map[string][]string{}
if req.Header != nil {
for name, headers := range req.Header {
passedHeaders[name] = headers
}
}
passedCookies = req.Cookies()
passedUsername, passedPassword, _ = req.BasicAuth()
err := req.ParseMultipartForm(4096)
if err != nil {
t.FailNow()
}
multipartFile, multipartHeader, err = req.FormFile("Field1")
if err != nil {
t.FailNow()
}
defer req.Body.Close()
passedFileContents, err = ioutil.ReadAll(multipartFile)
if err != nil {
t.FailNow()
}
rw.Write([]byte("OK"))
}))
// Close the server when test finishes
defer server.Close()
testFile, err := ioutil.TempFile("", "testFileUpload")
if err != nil {
t.FailNow()
}
defer os.RemoveAll(testFile.Name()) // clean up
fileContents, err := ioutil.ReadFile(testFile.Name())
if err != nil {
t.FailNow()
}
tt := []struct {
clientOptions ClientOptions
method string
body io.Reader
header http.Header
cookies []*http.Cookie
expected string
}{
{clientOptions: ClientOptions{}, method: "POST", expected: "OK"},
{clientOptions: ClientOptions{}, method: "POST", header: map[string][]string{"Testheader": []string{"Test1", "Test2"}}, expected: "OK"},
{clientOptions: ClientOptions{}, cookies: []*http.Cookie{{Name: "TestCookie1", Value: "TestValue1"}, {Name: "TestCookie2", Value: "TestValue2"}}, method: "POST", expected: "OK"},
{clientOptions: ClientOptions{Username: "TestUser", Password: "TestPwd"}, method: "POST", expected: "OK"},
}
client := Client{logger: log.Entry().WithField("package", "SAP/jenkins-library/pkg/http")}
for key, test := range tt {
t.Run(fmt.Sprintf("Row %v", key+1), func(t *testing.T) {
client.SetOptions(test.clientOptions)
response, err := client.UploadFile(server.URL, testFile.Name(), "Field1", test.header, test.cookies)
assert.NoError(t, err, "Error occurred but none expected")
content, err := ioutil.ReadAll(response.Body)
assert.NoError(t, err, "Error occurred but none expected")
assert.Equal(t, test.expected, string(content), "Returned content incorrect")
response.Body.Close()
assert.Equal(t, testFile.Name(), multipartHeader.Filename, "Uploaded file incorrect")
assert.Equal(t, fileContents, passedFileContents, "Uploaded file incorrect")
for k, h := range test.header {
assert.Containsf(t, passedHeaders, k, "Header %v not contained", k)
assert.Equalf(t, h, passedHeaders[k], "Header %v contains different value")
}
if len(test.cookies) > 0 {
assert.Equal(t, test.cookies, passedCookies, "Passed cookies not correct")
}
if len(client.username) > 0 {
assert.Equal(t, client.username, passedUsername)
}
if len(client.password) > 0 {
assert.Equal(t, client.password, passedPassword)
}
})
}
}