Skip to content

Commit

Permalink
feat: Add ability to export via HTTP PUT with secret header support
Browse files Browse the repository at this point in the history
Added the new HTTPPut pipeline function.

closes #516

Signed-off-by: lenny <[email protected]>
  • Loading branch information
lenny committed Oct 15, 2020
1 parent c65eb72 commit 6104d07
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 29 deletions.
68 changes: 64 additions & 4 deletions appsdk/configurable.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,12 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPPost(parameters map[string]string

url, ok := parameters[Url]
if !ok {
dynamic.Sdk.LoggingClient.Error("Could not find " + Url)
dynamic.Sdk.LoggingClient.Error("HTTPPost Could not find " + Url)
return nil
}
mimeType, ok := parameters[MimeType]
if !ok {
dynamic.Sdk.LoggingClient.Error("Could not find " + MimeType)
dynamic.Sdk.LoggingClient.Error("HTTPPost Could not find " + MimeType)
return nil
}

Expand All @@ -241,7 +241,7 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPPost(parameters map[string]string
if ok {
persistOnError, err = strconv.ParseBool(value)
if err != nil {
dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("Could not parse '%s' to a bool for '%s' parameter", value, PersistOnError), "error", err)
dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("HTTPPost Could not parse '%s' to a bool for '%s' parameter", value, PersistOnError), "error", err)
return nil
}
}
Expand All @@ -257,7 +257,7 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPPost(parameters map[string]string
} else {
transform = transforms.NewHTTPSender(url, mimeType, persistOnError)
}
dynamic.Sdk.LoggingClient.Debug("HTTP Post Parameters", Url, transform.URL, MimeType, transform.MimeType)
dynamic.Sdk.LoggingClient.Debug("HTTPPost Parameters", Url, transform.URL, MimeType, transform.MimeType)
return transform.HTTPPost
}

Expand All @@ -277,6 +277,66 @@ func (dynamic AppFunctionsSDKConfigurable) HTTPPostXML(parameters map[string]str
return dynamic.HTTPPost(parameters)
}

// HTTPPost will send data from the previous function to the specified Endpoint via http PUT. If no previous function exists,
// then the event that triggered the pipeline will be used. Passing an empty string to the mimetype
// method will default to application/json.
// This function is a configuration function and returns a function pointer.
func (dynamic AppFunctionsSDKConfigurable) HTTPPut(parameters map[string]string) appcontext.AppFunction {
var err error

url, ok := parameters[Url]
if !ok {
dynamic.Sdk.LoggingClient.Error("HTTPPut Could not find " + Url)
return nil
}
mimeType, ok := parameters[MimeType]
if !ok {
dynamic.Sdk.LoggingClient.Error("HTTPPut Could not find " + MimeType)
return nil
}

// PersistOnError is optional and is false by default.
persistOnError := false
value, ok := parameters[PersistOnError]
if ok {
persistOnError, err = strconv.ParseBool(value)
if err != nil {
dynamic.Sdk.LoggingClient.Error(fmt.Sprintf("HTTPPut Could not parse '%s' to a bool for '%s' parameter", value, PersistOnError), "error", err)
return nil
}
}

url = strings.TrimSpace(url)
mimeType = strings.TrimSpace(mimeType)

secretHeaderName := parameters[SecretHeaderName]
secretPath := parameters[SecretPath]
var transform transforms.HTTPSender
if secretHeaderName != "" && secretPath != "" {
transform = transforms.NewHTTPSenderWithSecretHeader(url, mimeType, persistOnError, secretHeaderName, secretPath)
} else {
transform = transforms.NewHTTPSender(url, mimeType, persistOnError)
}
dynamic.Sdk.LoggingClient.Debug("HTTPPut Parameters", Url, transform.URL, MimeType, transform.MimeType)
return transform.HTTPPut
}

// HTTPPostJSON sends data from the previous function to the specified Endpoint via http POST with a mime type of application/json.
// If no previous function exists, then the event that triggered the pipeline will be used.
// This function is a configuration function and returns a function pointer.
func (dynamic AppFunctionsSDKConfigurable) HTTPPutJSON(parameters map[string]string) appcontext.AppFunction {
parameters[MimeType] = "application/json"
return dynamic.HTTPPut(parameters)
}

// HTTPPutXML sends data from the previous function to the specified Endpoint via http PUT with a mime type of application/xml.
// If no previous function exists, then the event that triggered the pipeline will be used.
// This function is a configuration function and returns a function pointer.
func (dynamic AppFunctionsSDKConfigurable) HTTPPutXML(parameters map[string]string) appcontext.AppFunction {
parameters[MimeType] = "application/xml"
return dynamic.HTTPPut(parameters)
}

// MQTTSend sends data from the previous function to the specified MQTT broker.
// If no previous function exists, then the event that triggered the pipeline will be used.
// This function is a configuration function and returns a function pointer.
Expand Down
2 changes: 1 addition & 1 deletion pkg/transforms/conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const (
)

func init() {
lc := logger.NewClient("app_functions_sdk_go", false, "./test.log", "DEBUG")
lc := logger.NewMockClient()
eventClient := coredata.NewEventClient(local.New("http://test" + clients.ApiEventRoute))
mockSP := newMockSecretProvider(lc, nil)

Expand Down
17 changes: 15 additions & 2 deletions pkg/transforms/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ func NewHTTPSender(url string, mimeType string, persistOnError bool) HTTPSender
PersistOnError: persistOnError,
}
}

// NewHTTPSenderWithSecretHeader creates, initializes and returns a new instance of HTTPSender configured to use a secret header
func NewHTTPSenderWithSecretHeader(url string, mimeType string, persistOnError bool, httpHeaderSecretName string, secretPath string) HTTPSender {
return HTTPSender{
URL: url,
Expand All @@ -60,6 +62,17 @@ func NewHTTPSenderWithSecretHeader(url string, mimeType string, persistOnError b
// If no previous function exists, then the event that triggered the pipeline will be used.
// An empty string for the mimetype will default to application/json.
func (sender HTTPSender) HTTPPost(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) {
return sender.httpSend(edgexcontext, params, http.MethodPost)
}

// HTTPPut will send data from the previous function to the specified Endpoint via http PUT.
// If no previous function exists, then the event that triggered the pipeline will be used.
// An empty string for the mimetype will default to application/json.
func (sender HTTPSender) HTTPPut(edgexcontext *appcontext.Context, params ...interface{}) (bool, interface{}) {
return sender.httpSend(edgexcontext, params, http.MethodPut)
}

func (sender HTTPSender) httpSend(edgexcontext *appcontext.Context, params []interface{}, method string) (bool, interface{}) {
if len(params) < 1 {
// We didn't receive a result
return false, errors.New("No Data Received")
Expand All @@ -80,7 +93,7 @@ func (sender HTTPSender) HTTPPost(edgexcontext *appcontext.Context, params ...in
}

client := &http.Client{}
req, err := http.NewRequest(http.MethodPost, sender.URL, bytes.NewReader(exportData))
req, err := http.NewRequest(method, sender.URL, bytes.NewReader(exportData))
if err != nil {
return false, err
}
Expand Down Expand Up @@ -119,8 +132,8 @@ func (sender HTTPSender) HTTPPost(edgexcontext *appcontext.Context, params ...in
}

return true, bodyBytes

}

func (sender HTTPSender) determineIfUsingSecrets() (bool, error) {
//check if one field but not others are provided for secrets
if sender.SecretPath != "" && sender.SecretHeaderName == "" {
Expand Down
92 changes: 70 additions & 22 deletions pkg/transforms/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,18 @@ var logClient logger.LoggingClient
var config *common.ConfigurationStruct

func TestMain(m *testing.M) {
logClient = logger.NewClient("app_functions_sdk_go", false, "./test.log", "DEBUG")
logClient = logger.NewMockClient()
config = &common.ConfigurationStruct{}
m.Run()
}

func TestHTTPPost(t *testing.T) {

func TestHTTPPostPut(t *testing.T) {
context.CorrelationID = "123"

var methodUsed string

handler := func(w http.ResponseWriter, r *http.Request) {
methodUsed = r.Method

if r.URL.EscapedPath() == badPath {
w.WriteHeader(http.StatusNotFound)
Expand Down Expand Up @@ -84,31 +86,44 @@ func TestHTTPPost(t *testing.T) {
require.NoError(t, err)

tests := []struct {
Name string
Path string
PersistOnFail bool
RetryDataSet bool
Name string
Path string
PersistOnFail bool
RetryDataSet bool
ExpectedMethod string
}{
{"Successful post", path, true, false},
{"Failed Post no persist", badPath, false, false},
{"Failed Post with persist", badPath, true, true},
{"Successful POST", path, true, false, http.MethodPost},
{"Failed POST no persist", badPath, false, false, http.MethodPost},
{"Failed POST with persist", badPath, true, true, http.MethodPost},
{"Successful PUT", path, false, false, http.MethodPut},
{"Failed PUT no persist", badPath, false, false, http.MethodPut},
{"Failed PUT with persist", badPath, true, true, http.MethodPut},
}

for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
context.RetryData = nil
methodUsed = ""
sender := NewHTTPSender(`http://`+url.Host+test.Path, "", test.PersistOnFail)

sender.HTTPPost(context, msgStr)
if test.ExpectedMethod == http.MethodPost {
sender.HTTPPost(context, msgStr)
} else {
sender.HTTPPut(context, msgStr)
}

assert.Equal(t, test.RetryDataSet, context.RetryData != nil)
assert.Equal(t, test.ExpectedMethod, methodUsed)
})
}
}

func TestHTTPPostWithSecrets(t *testing.T) {
func TestHTTPPostPutWithSecrets(t *testing.T) {
var methodUsed string

expectedValue := "value"
handler := func(w http.ResponseWriter, r *http.Request) {
methodUsed = r.Method

if r.URL.EscapedPath() == badPath {
w.WriteHeader(http.StatusNotFound)
Expand Down Expand Up @@ -138,31 +153,44 @@ func TestHTTPPostWithSecrets(t *testing.T) {
SecretPath string
ExpectToContinue bool
ExpectedErrorMessage string
ExpectedMethod string
}{
{"unsuccessful post w/o secret header name", path, "", "/path", false, "SecretPath was specified but no header name was provided"},
{"unsuccessful post w/o secret path name", path, "Secret-Header-Name", "", false, "HTTP Header Secret Name was provided but no SecretPath was provided"},
{"successful post with secrets", path, "Secret-Header-Name", "/path", true, ""},
{"successful post without secrets", path, "", "", true, ""},
{"unsuccessful post with secrets - retrieval fails", path, "Secret-Header-Name-2", "/path", false, "FAKE NOT FOUND ERROR"},
{"unsuccessful POST w/o secret header name", path, "", "/path", false, "SecretPath was specified but no header name was provided", ""},
{"unsuccessful POST w/o secret path name", path, "Secret-Header-Name", "", false, "HTTP Header Secret Name was provided but no SecretPath was provided", ""},
{"successful POST with secrets", path, "Secret-Header-Name", "/path", true, "", http.MethodPost},
{"successful POST without secrets", path, "", "", true, "", http.MethodPost},
{"unsuccessful POST with secrets - retrieval fails", path, "Secret-Header-Name-2", "/path", false, "FAKE NOT FOUND ERROR", ""},
{"unsuccessful PUT w/o secret header name", path, "", "/path", false, "SecretPath was specified but no header name was provided", ""},
{"unsuccessful PUT w/o secret path name", path, "Secret-Header-Name", "", false, "HTTP Header Secret Name was provided but no SecretPath was provided", ""},
{"successful PUT with secrets", path, "Secret-Header-Name", "/path", true, "", http.MethodPut},
{"successful PUT without secrets", path, "", "", true, "", http.MethodPut},
{"unsuccessful PUT with secrets - retrieval fails", path, "Secret-Header-Name-2", "/path", false, "FAKE NOT FOUND ERROR", ""},
}

for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
var sender HTTPSender
methodUsed = ""
sender := NewHTTPSenderWithSecretHeader(`http://`+url.Host+test.Path, "", false, test.SecretHeaderName, test.SecretPath)

sender = NewHTTPSenderWithSecretHeader(`http://`+url.Host+test.Path, "", false, test.SecretHeaderName, test.SecretPath)
var continuePipeline bool
var err interface{}

if test.ExpectedMethod == http.MethodPost {
continuePipeline, err = sender.HTTPPost(context, msgStr)
} else {
continuePipeline, err = sender.HTTPPut(context, msgStr)
}

continuePipeline, err := sender.HTTPPost(context, msgStr)
assert.Equal(t, test.ExpectToContinue, continuePipeline)
if !test.ExpectToContinue {
require.EqualError(t, err.(error), test.ExpectedErrorMessage)
}
assert.Equal(t, test.ExpectedMethod, methodUsed)
})
}
}

func TestHTTPPostNoParameterPassed(t *testing.T) {

sender := NewHTTPSender("", "", false)
continuePipeline, result := sender.HTTPPost(context)

Expand All @@ -171,8 +199,16 @@ func TestHTTPPostNoParameterPassed(t *testing.T) {
assert.Equal(t, "No Data Received", result.(error).Error())
}

func TestHTTPPostInvalidParameter(t *testing.T) {
func TestHTTPPutNoParameterPassed(t *testing.T) {
sender := NewHTTPSender("", "", false)
continuePipeline, result := sender.HTTPPut(context)

assert.False(t, continuePipeline, "Pipeline should stop")
assert.Error(t, result.(error), "Result should be an error")
assert.Equal(t, "No Data Received", result.(error).Error())
}

func TestHTTPPostInvalidParameter(t *testing.T) {
sender := NewHTTPSender("", "", false)
// Channels are not marshalable to JSON and generate an error
data := make(chan int)
Expand All @@ -184,6 +220,18 @@ func TestHTTPPostInvalidParameter(t *testing.T) {
"passed in data must be of type []byte, string, or support marshaling to JSON", result.(error).Error())
}

func TestHTTPPutInvalidParameter(t *testing.T) {
sender := NewHTTPSender("", "", false)
// Channels are not marshalable to JSON and generate an error
data := make(chan int)
continuePipeline, result := sender.HTTPPut(context, data)

assert.False(t, continuePipeline, "Pipeline should stop")
assert.Error(t, result.(error), "Result should be an error")
assert.Equal(t, "marshaling input data to JSON failed, "+
"passed in data must be of type []byte, string, or support marshaling to JSON", result.(error).Error())
}

type mockSecretClient struct {
}

Expand Down

0 comments on commit 6104d07

Please sign in to comment.