mirror of
https://github.com/openfaas/faas.git
synced 2025-06-08 16:26:47 +00:00
581 lines
14 KiB
Go
581 lines
14 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"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestHandler_make(t *testing.T) {
|
|
config := WatchdogConfig{}
|
|
handler := makeRequestHandler(&config)
|
|
|
|
if handler == nil {
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
func TestHandler_TransferEncodingPassedToFunction(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := "Message"
|
|
req, err := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(body))
|
|
req.TransferEncoding = []string{
|
|
"chunked",
|
|
}
|
|
req.ContentLength = -1
|
|
|
|
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=-1`) {
|
|
t.Errorf(config.faasProcess+" should print: Http_ContentLength=-1, got: %s\n", val)
|
|
}
|
|
|
|
if !strings.Contains(val, "Http_Transfer_Encoding") {
|
|
t.Errorf(config.faasProcess+" should print: Http_Transfer_Encoding=chunked, got: %s\n", val)
|
|
}
|
|
|
|
}
|
|
|
|
func TestHandler_HasCustomHeaderInFunction_WithCgi_Mode(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := ""
|
|
req, err := http.NewRequest(http.MethodPost, "/", 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_Content_Length=0") {
|
|
t.Errorf(config.faasProcess+" should print: Http_Content_Length=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(http.MethodPost, "/", 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, fmt.Sprintf("Http_Content_Length=%d", len(body))) {
|
|
t.Errorf("'env' should printed: Http_Content_Length=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_HasHostHeaderWhenSet(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := "test"
|
|
req, err := http.NewRequest(http.MethodPost, "http://gateway/function", bytes.NewBufferString(body))
|
|
|
|
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_Host=%s", req.URL.Host)) {
|
|
t.Errorf("'env' should have printed: Http_Host=0, got: %s\n", val)
|
|
}
|
|
}
|
|
|
|
func TestHandler_HostHeader_Empty_WhenNotSet(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := "test"
|
|
req, err := http.NewRequest(http.MethodPost, "/function", bytes.NewBufferString(body))
|
|
|
|
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_Host=%s", req.URL.Host)) {
|
|
t.Errorf("Http_Host should not have been given, but was: %s\n", val)
|
|
}
|
|
}
|
|
|
|
func TestHandler_StderrWritesToStderr_CombinedOutput_False(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
b := bytes.NewBuffer([]byte{})
|
|
log.SetOutput(b)
|
|
|
|
body := ""
|
|
req, err := http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(body))
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "stat x",
|
|
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)
|
|
|
|
want := "No such file or directory"
|
|
if strings.Contains(stderrVal, want) == false {
|
|
t.Logf("Stderr should have contained error from function \"%s\", but was: %s", want, 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(http.MethodPost, "/", bytes.NewBufferString(body))
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
config := WatchdogConfig{
|
|
faasProcess: "stat x",
|
|
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)
|
|
stdErrWant := "No such file or directory"
|
|
if strings.Contains(stderrVal, stdErrWant) {
|
|
t.Logf("stderr should have not included any function errors, but did")
|
|
t.Fail()
|
|
}
|
|
|
|
bodyBytes, _ := ioutil.ReadAll(rr.Body)
|
|
bodyStr := string(bodyBytes)
|
|
stdOuputWant := `exit status 1`
|
|
if strings.Contains(bodyStr, stdOuputWant) == false {
|
|
t.Logf("response want: %s, got: %s", stdOuputWant, bodyStr)
|
|
t.Fail()
|
|
}
|
|
if strings.Contains(bodyStr, stdErrWant) == false {
|
|
t.Logf("response want: %s, got: %s", stdErrWant, bodyStr)
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
func TestHandler_DoesntHaveCustomHeaderInFunction_WithoutCgi_Mode(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := ""
|
|
req, err := http.NewRequest(http.MethodPost, "/", 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(http.MethodPost, "/", 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{http.MethodPost}
|
|
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{http.MethodPost, http.MethodPut, http.MethodDelete}
|
|
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(http.MethodGet, "/", 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)
|
|
}
|
|
}
|
|
|
|
func TestHealthHandler_StatusOK_LockFilePresent(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
present := lockFilePresent()
|
|
|
|
if present == false {
|
|
if _, err := createLockFile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "/_/health", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handler := makeHealthHandler()
|
|
handler(rr, req)
|
|
|
|
required := http.StatusOK
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code: got %v, but wanted %v", status, required)
|
|
}
|
|
|
|
}
|
|
|
|
func TestHealthHandler_StatusInternalServerError_LockFileNotPresent(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
if lockFilePresent() == true {
|
|
if err := removeLockFile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequest(http.MethodGet, "/_/health", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
handler := makeHealthHandler()
|
|
handler(rr, req)
|
|
|
|
required := http.StatusServiceUnavailable
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code - got: %v, want: %v", status, required)
|
|
}
|
|
}
|
|
|
|
func TestHealthHandler_SatusMethoNotAllowed_ForWriteableVerbs(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
verbs := []string{http.MethodPost, http.MethodPut, http.MethodDelete}
|
|
|
|
for _, verb := range verbs {
|
|
req, err := http.NewRequest(verb, "/_/health", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
handler := makeHealthHandler()
|
|
handler(rr, req)
|
|
|
|
required := http.StatusMethodNotAllowed
|
|
if status := rr.Code; status != required {
|
|
t.Errorf("handler returned wrong status code: got %v, but wanted %v", status, required)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandler_HasFullPathAndQueryInFunction_WithCgi_Mode(t *testing.T) {
|
|
rr := httptest.NewRecorder()
|
|
|
|
body := ""
|
|
wantPath := "/my/full/path"
|
|
wantQuery := "q=x"
|
|
requestURI := wantPath + "?" + wantQuery
|
|
req, err := http.NewRequest(http.MethodPost, requestURI, bytes.NewBufferString(body))
|
|
|
|
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_Path="+wantPath) {
|
|
t.Errorf(config.faasProcess+" should print: Http_Path="+wantPath+", got: %s\n", val)
|
|
}
|
|
|
|
if !strings.Contains(val, "Http_Query="+wantQuery) {
|
|
t.Errorf(config.faasProcess+" should print: Http_Query="+wantQuery+", got: %s\n", val)
|
|
}
|
|
}
|
|
|
|
func removeLockFile() error {
|
|
path := filepath.Join(os.TempDir(), ".lock")
|
|
log.Printf("Removing lock-file : %s\n", path)
|
|
removeErr := os.Remove(path)
|
|
return removeErr
|
|
}
|