// Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. package webhook_test import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "testing" "text/template" "time" "github.com/Masterminds/sprig/v3" "github.com/aws/aws-node-termination-handler/pkg/config" "github.com/aws/aws-node-termination-handler/pkg/ec2metadata" "github.com/aws/aws-node-termination-handler/pkg/monitor" h "github.com/aws/aws-node-termination-handler/pkg/test" "github.com/aws/aws-node-termination-handler/pkg/webhook" "github.com/rs/zerolog/log" ) const ( testDateFormat = "02 Jan 2006 15:04:05 GMT" testWebhookHeaders = `{"Content-type":"application/json"}` testWebhookTemplate = `{"text":"[NTH][Instance Interruption] EventID: {{ .EventID | trimPrefix "event" }} - Kind: {{ .Kind | lower }} - Node: {{ .NodeName }} - Description: {{ .Description }} - Start Time: {{ .StartTime }}"}` ) func parseScheduledEventTime(inputTime string) time.Time { scheduledTime, _ := time.Parse(testDateFormat, inputTime) return scheduledTime } func getExpectedMessage(t *testing.T, event *monitor.InterruptionEvent) string { webhookTemplate, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(testWebhookTemplate) if err != nil { log.Err(err).Msg("Webhook Error: Template parsing failed") return "" } var byteBuffer bytes.Buffer err = webhookTemplate.Execute(&byteBuffer, event) h.Ok(t, err) m := map[string]interface{}{} if err := json.Unmarshal(byteBuffer.Bytes(), &m); err != nil { return "" } return fmt.Sprintf("%v", m["text"]) } func TestPostSuccess(t *testing.T) { var requestPath string = "/some/path" event := &monitor.InterruptionEvent{ EventID: "instance-event-0d59937288b749b32", Kind: "SCHEDULED_EVENT", Description: "Scheduled event will occur", State: "active", StartTime: parseScheduledEventTime("21 Jan 2019 09:00:43 GMT"), EndTime: parseScheduledEventTime("21 Jan 2019 09:17:23 GMT"), NodeName: "e2e-test-abcd", } server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { h.Equals(t, req.Method, "POST") h.Equals(t, req.URL.String(), requestPath) // Test request headers headerMap := make(map[string]interface{}) if err := json.Unmarshal([]byte(testWebhookHeaders), &headerMap); err != nil { t.Error("Unable to parse webhook headers") } h.Equals(t, req.Header.Get("Content-type"), headerMap["Content-type"]) // Test request body requestBody, err := io.ReadAll(req.Body) if err != nil { t.Error("Unable to read request body.") } requestMap := map[string]interface{}{} if err := json.Unmarshal([]byte(requestBody), &requestMap); err != nil { t.Error("Unable to parse request body to json.") } h.Equals(t, getExpectedMessage(t, event), requestMap["text"]) _, err = rw.Write([]byte(`OK`)) h.Ok(t, err) })) defer server.Close() nthconfig := config.Config{ WebhookURL: server.URL + requestPath, WebhookHeaders: testWebhookHeaders, WebhookTemplate: testWebhookTemplate, } nodeMetadata := ec2metadata.NodeMetadata{} webhook.Post(nodeMetadata, event, nthconfig) } func TestPostTemplateParseError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { t.Error("Request made with invalid webhook") })) defer server.Close() event := &monitor.InterruptionEvent{} nthconfig := config.Config{ WebhookURL: server.URL, WebhookTemplate: "{{ ", } nodeMetadata := ec2metadata.NodeMetadata{} webhook.Post(nodeMetadata, event, nthconfig) } func TestPostTemplateExecutionError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { t.Error("Request made with invalid webhook") })) defer server.Close() event := &monitor.InterruptionEvent{} nthconfig := config.Config{ WebhookURL: server.URL, WebhookTemplate: `{{.cat}}`, } nodeMetadata := ec2metadata.NodeMetadata{} webhook.Post(nodeMetadata, event, nthconfig) } func TestPostNewHttpRequestError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { t.Error("Request made with invalid webhook") })) defer server.Close() event := &monitor.InterruptionEvent{} nthconfig := config.Config{ WebhookURL: "\t", WebhookTemplate: testWebhookTemplate, } nodeMetadata := ec2metadata.NodeMetadata{} webhook.Post(nodeMetadata, event, nthconfig) } func TestPostHeaderParseFail(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { t.Error("Request made with invalid webhook") })) defer server.Close() event := &monitor.InterruptionEvent{} nthconfig := config.Config{ WebhookURL: server.URL, WebhookTemplate: testWebhookTemplate, } nodeMetadata := ec2metadata.NodeMetadata{} webhook.Post(nodeMetadata, event, nthconfig) } func TestPostTimeout(t *testing.T) { var requestCount int = 0 server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { requestCount++ time.Sleep(6 * time.Second) })) defer server.Close() event := &monitor.InterruptionEvent{} nthconfig := config.Config{ WebhookURL: server.URL, WebhookTemplate: testWebhookTemplate, WebhookHeaders: testWebhookHeaders, } nodeMetadata := ec2metadata.NodeMetadata{} webhook.Post(nodeMetadata, event, nthconfig) h.Equals(t, 1, requestCount) } func TestPostBadResponseCode(t *testing.T) { var requestCount int = 0 server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { requestCount++ http.Error(rw, "404 page not found", http.StatusNotFound) })) defer server.Close() event := &monitor.InterruptionEvent{} nthconfig := config.Config{ WebhookURL: server.URL, WebhookTemplate: testWebhookTemplate, WebhookHeaders: testWebhookHeaders, } nodeMetadata := ec2metadata.NodeMetadata{} webhook.Post(nodeMetadata, event, nthconfig) h.Equals(t, 1, requestCount) } func TestValidateWebhookConfig(t *testing.T) { var nthConfig = config.Config{} err := webhook.ValidateWebhookConfig(nthConfig) h.Ok(t, err) nthConfig.WebhookURL = "http://123.123.123" nthConfig.WebhookTemplate = "{{ " err = webhook.ValidateWebhookConfig(nthConfig) h.Assert(t, err != nil, "Failed to return error for failing to parse webhook template") nthConfig.WebhookTemplate = "{{.cat}}" err = webhook.ValidateWebhookConfig(nthConfig) h.Assert(t, err != nil, "Failed to return error for failing to execute webhook template") nthConfig.WebhookTemplate = testWebhookTemplate err = webhook.ValidateWebhookConfig(nthConfig) h.Ok(t, err) }