package tests import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "path" "strings" "testing" "time" "github.com/fnproject/fn/api/models" ) // TODO deprecate with routes func TestCanExecuteFunction(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() rt := ensureRoute(t) lb, err := LB() if err != nil { t.Fatalf("Got unexpected error: %v", err) } u := url.URL{ Scheme: "http", Host: lb, } u.Path = path.Join(u.Path, "r", appName, rt.Path) body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callFN(ctx, u.String(), content, output, "POST") if err != nil { t.Fatalf("Got unexpected error: %v", err) } echo, err := getEchoContent(output.Bytes()) if err != nil || echo != "HelloWorld" { t.Fatalf("getEchoContent/HelloWorld check failed on %v", output) } if resp.StatusCode != http.StatusOK { t.Fatalf("StatusCode check failed on %v", resp.StatusCode) } // Now let's check FN_CHEESE, since LB and runners have override/extension mechanism // to insert FN_CHEESE into config cheese, err := getConfigContent("FN_CHEESE", output.Bytes()) if err != nil || cheese != "Tete de Moine" { t.Fatalf("getConfigContent/FN_CHEESE check failed (%v) on %v", err, output) } // Now let's check FN_WINE, since runners have override to insert this. wine, err := getConfigContent("FN_WINE", output.Bytes()) if err != nil || wine != "1982 Margaux" { t.Fatalf("getConfigContent/FN_WINE check failed (%v) on %v", err, output) } } func TestCanExecuteBigOutput(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() rt := ensureRoute(t) lb, err := LB() if err != nil { t.Fatalf("Got unexpected error: %v", err) } u := url.URL{ Scheme: "http", Host: lb, } u.Path = path.Join(u.Path, "r", appName, rt.Path) // Approx 5.3MB output body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true, "trailerRepeat": 410000}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callFN(ctx, u.String(), content, output, "POST") if err != nil { t.Fatalf("Got unexpected error: %v", err) } t.Logf("getEchoContent/HelloWorld size %d", len(output.Bytes())) echo, err := getEchoContent(output.Bytes()) if err != nil || echo != "HelloWorld" { t.Fatalf("getEchoContent/HelloWorld check failed on %v", output) } if resp.StatusCode != http.StatusOK { t.Fatalf("StatusCode check failed on %v", resp.StatusCode) } } func TestCanExecuteTooBigOutput(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() rt := ensureRoute(t) lb, err := LB() if err != nil { t.Fatalf("Got unexpected error: %v", err) } u := url.URL{ Scheme: "http", Host: lb, } u.Path = path.Join(u.Path, "r", appName, rt.Path) // > 6MB output body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true, "trailerRepeat": 600000}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callFN(ctx, u.String(), content, output, "POST") if err != nil { t.Fatalf("Got unexpected error: %v", err) } exp := "{\"error\":{\"message\":\"function response too large\"}}\n" actual := output.String() if !strings.Contains(exp, actual) || len(exp) != len(actual) { t.Fatalf("Assertion error.\n\tExpected: %v\n\tActual: %v", exp, output.String()) } if resp.StatusCode != http.StatusBadGateway { t.Fatalf("StatusCode check failed on %v", resp.StatusCode) } } func TestCanExecuteEmptyOutput(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() rt := ensureRoute(t) lb, err := LB() if err != nil { t.Fatalf("Got unexpected error: %v", err) } u := url.URL{ Scheme: "http", Host: lb, } u.Path = path.Join(u.Path, "r", appName, rt.Path) // empty body output body := `{"sleepTime": 0, "isDebug": true, "isEmptyBody": true}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callFN(ctx, u.String(), content, output, "POST") if err != nil { t.Fatalf("Got unexpected error: %v", err) } actual := output.String() if 0 != len(actual) { t.Fatalf("Assertion error.\n\tExpected empty\n\tActual: %v", output.String()) } if resp.StatusCode != http.StatusOK { t.Fatalf("StatusCode check failed on %v", resp.StatusCode) } } func TestBasicConcurrentExecution(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() rt := ensureRoute(t) lb, err := LB() if err != nil { t.Fatalf("Got unexpected error: %v", err) } u := url.URL{ Scheme: "http", Host: lb, } u.Path = path.Join(u.Path, "r", appName, rt.Path) results := make(chan error) concurrentFuncs := 10 for i := 0; i < concurrentFuncs; i++ { go func() { body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callFN(ctx, u.String(), content, output, "POST") if err != nil { results <- fmt.Errorf("Got unexpected error: %v", err) return } echo, err := getEchoContent(output.Bytes()) if err != nil || echo != "HelloWorld" { results <- fmt.Errorf("Assertion error.\n\tActual: %v", output.String()) return } if resp.StatusCode != http.StatusOK { results <- fmt.Errorf("StatusCode check failed on %v", resp.StatusCode) return } results <- nil }() } for i := 0; i < concurrentFuncs; i++ { err := <-results if err != nil { t.Fatalf("Error in basic concurrency execution test: %v", err) } } } func TestSaturatedSystem(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() rt := &models.Route{ Path: routeName, Timeout: 1, Image: "fnproject/fn-test-utils", Format: "json", Memory: 300, Type: "sync", } rt = ensureRoute(t, rt) lb, err := LB() if err != nil { t.Fatalf("Got unexpected error: %v", err) } u := url.URL{ Scheme: "http", Host: lb, } u.Path = path.Join(u.Path, "r", appName, rt.Path) body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callFN(ctx, u.String(), content, output, "POST") if resp != nil || err == nil || ctx.Err() == nil { t.Fatalf("Expected response: %v err:%v", resp, err) } } func callFN(ctx context.Context, u string, content io.Reader, output io.Writer, method string) (*http.Response, error) { if method == "" { if content == nil { method = "GET" } else { method = "POST" } } req, err := http.NewRequest(method, u, content) if err != nil { return nil, fmt.Errorf("error running route: %s", err) } req.Header.Set("Content-Type", "application/json") req = req.WithContext(ctx) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("error running route: %s", err) } io.Copy(output, resp.Body) return resp, nil } func ensureRoute(t *testing.T, rts ...*models.Route) *models.Route { var rt *models.Route if len(rts) > 0 { rt = rts[0] } else { rt = &models.Route{ Path: routeName + "yabbadabbadoo", Image: image, Format: format, Memory: memory, Type: typ, } } var wrapped struct { Route *models.Route `json:"route"` } wrapped.Route = rt var buf bytes.Buffer err := json.NewEncoder(&buf).Encode(wrapped) if err != nil { t.Fatal("error encoding body", err) } urlStr := host() + "/v1/apps/" + appName + "/routes" + rt.Path u, err := url.Parse(urlStr) if err != nil { t.Fatal("error creating url", urlStr, err) } req, err := http.NewRequest("PUT", u.String(), &buf) if err != nil { t.Fatal("error creating request", err) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal("error creating route", err) } buf.Reset() io.Copy(&buf, resp.Body) if resp.StatusCode != 200 { t.Fatal("error creating/updating app or otherwise ensuring it exists:", resp.StatusCode, buf.String()) } wrapped.Route = nil err = json.NewDecoder(&buf).Decode(&wrapped) if err != nil { t.Fatal("error decoding response") } return wrapped.Route }