mirror of
https://github.com/openfaas/faas.git
synced 2025-06-10 17:26:47 +00:00
This enables an often-requested feature to separate stderr from stdout within function responses. New flag combine_output is on by default to match existing behaviour. When combine_output is set to false it redirects stderr to the container logs rather than combining it into the function response. Tested with unit tests for default behaviour and new behaviour. Signed-off-by: Alex Ellis (VMware) <alexellis2@gmail.com>
360 lines
8.4 KiB
Go
360 lines
8.4 KiB
Go
// Copyright (c) Alex Ellis 2017. All rights reserved.
|
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestHandler_make(t *testing.T) {
|
|
config := WatchdogConfig{}
|
|
handler := makeRequestHandler(&config)
|
|
|
|
if handler == nil {
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
func TestHandler_HasCustomHeaderInFunction_WithCgi_Mode(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := ""
|
|
req, err := http.NewRequest("POST", "/", bytes.NewBufferString(body))
|
|
req.Header.Add("custom-header", "value")
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "env",
|
|
cgiHeaders: true,
|
|
}
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusOK
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code - got: %v, want: %v",
|
|
status, required)
|
|
}
|
|
|
|
read, _ := ioutil.ReadAll(rr.Body)
|
|
val := string(read)
|
|
if !strings.Contains(val, "Http_ContentLength=0") {
|
|
t.Errorf(config.faasProcess+" should print: Http_ContentLength=0, got: %s\n", val)
|
|
}
|
|
if !strings.Contains(val, "Http_Custom_Header") {
|
|
t.Errorf(config.faasProcess+" should print: Http_Custom_Header, got: %s\n", val)
|
|
}
|
|
|
|
seconds := rr.Header().Get("X-Duration-Seconds")
|
|
if len(seconds) == 0 {
|
|
t.Errorf(config.faasProcess + " should have given a duration as an X-Duration-Seconds header\n")
|
|
}
|
|
}
|
|
|
|
func TestHandler_HasCustomHeaderInFunction_WithCgiMode_AndBody(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := "test"
|
|
req, err := http.NewRequest("POST", "/", bytes.NewBufferString(body))
|
|
req.Header.Add("custom-header", "value")
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "env",
|
|
cgiHeaders: true,
|
|
}
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusOK
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code - got: %v, want: %v",
|
|
status, required)
|
|
}
|
|
|
|
read, _ := ioutil.ReadAll(rr.Body)
|
|
val := string(read)
|
|
if !strings.Contains(val, fmt.Sprintf("Http_ContentLength=%d", len(body))) {
|
|
t.Errorf("'env' should printed: Http_ContentLength=0, got: %s\n", val)
|
|
}
|
|
if !strings.Contains(val, "Http_Custom_Header") {
|
|
t.Errorf("'env' should printed: Http_Custom_Header, got: %s\n", val)
|
|
}
|
|
|
|
seconds := rr.Header().Get("X-Duration-Seconds")
|
|
if len(seconds) == 0 {
|
|
t.Errorf("Exec of cat should have given a duration as an X-Duration-Seconds header\n")
|
|
}
|
|
}
|
|
|
|
func TestHandler_StderrWritesToStderr_CombinedOutput_False(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
b := bytes.NewBuffer([]byte{})
|
|
log.SetOutput(b)
|
|
|
|
body := ""
|
|
req, err := http.NewRequest("POST", "/", bytes.NewBufferString(body))
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "man badtopic",
|
|
cgiHeaders: true,
|
|
combineOutput: false,
|
|
}
|
|
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusInternalServerError
|
|
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code - got: %v, want: %v",
|
|
status, required)
|
|
}
|
|
|
|
log.SetOutput(os.Stderr)
|
|
|
|
stderrBytes, _ := ioutil.ReadAll(b)
|
|
stderrVal := string(stderrBytes)
|
|
|
|
if strings.Contains(stderrVal, "No manual entry for") == false {
|
|
t.Logf("Stderr should have contained error from function \"No manual entry for\", but was: %s", stderrVal)
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
func TestHandler_StderrWritesToResponse_CombinedOutput_True(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
b := bytes.NewBuffer([]byte{})
|
|
log.SetOutput(b)
|
|
|
|
body := ""
|
|
req, err := http.NewRequest("POST", "/", bytes.NewBufferString(body))
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "man badtopic",
|
|
cgiHeaders: true,
|
|
combineOutput: true,
|
|
}
|
|
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusInternalServerError
|
|
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code - got: %v, want: %v",
|
|
status, required)
|
|
}
|
|
|
|
log.SetOutput(os.Stderr)
|
|
|
|
stderrBytes, _ := ioutil.ReadAll(b)
|
|
stderrVal := string(stderrBytes)
|
|
|
|
if strings.Contains(stderrVal, "No manual entry for") {
|
|
t.Logf("stderr should have not included any function errors, but did")
|
|
t.Fail()
|
|
}
|
|
|
|
bodyBytes, _ := ioutil.ReadAll(rr.Body)
|
|
bodyStr := string(bodyBytes)
|
|
want := `exit status 1
|
|
No manual entry for badtopic`
|
|
if strings.Contains(bodyStr, want) == false {
|
|
t.Logf("response want: %s, got: %s", want, bodyStr)
|
|
t.Fail()
|
|
}
|
|
|
|
}
|
|
|
|
func TestHandler_DoesntHaveCustomHeaderInFunction_WithoutCgi_Mode(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := ""
|
|
req, err := http.NewRequest("POST", "/", bytes.NewBufferString(body))
|
|
req.Header.Add("custom-header", "value")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "env",
|
|
cgiHeaders: false,
|
|
}
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusOK
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code - got: %v, want: %v",
|
|
status, required)
|
|
}
|
|
|
|
read, _ := ioutil.ReadAll(rr.Body)
|
|
val := string(read)
|
|
if strings.Contains(val, "Http_Custom_Header") {
|
|
t.Errorf("'env' should not have printed: Http_Custom_Header, got: %s\n", val)
|
|
|
|
}
|
|
|
|
seconds := rr.Header().Get("X-Duration-Seconds")
|
|
if len(seconds) == 0 {
|
|
t.Errorf("Exec of cat should have given a duration as an X-Duration-Seconds header\n")
|
|
}
|
|
}
|
|
|
|
func TestHandler_HasXDurationSecondsHeader(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := "hello"
|
|
req, err := http.NewRequest("POST", "/", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "cat",
|
|
}
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusOK
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code - got: %v, want: %v",
|
|
status, required)
|
|
}
|
|
|
|
seconds := rr.Header().Get("X-Duration-Seconds")
|
|
if len(seconds) == 0 {
|
|
t.Errorf("Exec of " + config.faasProcess + " should have given a duration as an X-Duration-Seconds header")
|
|
}
|
|
}
|
|
|
|
func TestHandler_RequestTimeoutFailsForExceededDuration(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
verbs := []string{"POST"}
|
|
for _, verb := range verbs {
|
|
|
|
body := "hello"
|
|
req, err := http.NewRequest(verb, "/", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "sleep 2",
|
|
execTimeout: time.Duration(100) * time.Millisecond,
|
|
}
|
|
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusRequestTimeout
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code for verb [%s] - got: %v, want: %v",
|
|
verb, status, required)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandler_StatusOKAllowed_ForWriteableVerbs(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
verbs := []string{"POST", "PUT", "UPDATE", "DELETE"}
|
|
for _, verb := range verbs {
|
|
|
|
body := "hello"
|
|
req, err := http.NewRequest(verb, "/", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "cat",
|
|
}
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusOK
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code for verb [%s] - got: %v, want: %v",
|
|
verb, status, required)
|
|
}
|
|
|
|
buf, _ := ioutil.ReadAll(rr.Body)
|
|
val := string(buf)
|
|
if val != body {
|
|
t.Errorf("Exec of cat did not return input value, %s", val)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandler_StatusMethodNotAllowed_ForUnknown(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
req, err := http.NewRequest("UNKNOWN", "/", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{}
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusMethodNotAllowed
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code: got %v, want: %v",
|
|
status, required)
|
|
}
|
|
}
|
|
|
|
func TestHandler_StatusOKForGETAndNoBody(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
req, err := http.NewRequest("GET", "/", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
// writeDebug: true,
|
|
faasProcess: "date",
|
|
}
|
|
|
|
handler := makeRequestHandler(&config)
|
|
handler(rr, req)
|
|
|
|
required := http.StatusOK
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code: got %v, want: %v",
|
|
status, required)
|
|
}
|
|
}
|