package tests import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "log" "net/http" "net/url" "path" "strings" "testing" "time" "github.com/fnproject/fn/api/datastore/datastoretest" "github.com/fnproject/fn/api/models" ) // See fn-test-utils for json response func getEchoContent(respBytes []byte) (string, error) { var respJs map[string]interface{} err := json.Unmarshal(respBytes, &respJs) if err != nil { return "", err } req, ok := respJs["request"].(map[string]interface{}) if !ok { return "", errors.New("unexpected json: request map") } echo, ok := req["echoContent"].(string) if !ok { return "", errors.New("unexpected json: echoContent string") } return echo, nil } // See fn-test-utils for json response func getConfigContent(key string, respBytes []byte) (string, error) { var respJs map[string]interface{} err := json.Unmarshal(respBytes, &respJs) if err != nil { return "", err } cfg, ok := respJs["config"].(map[string]interface{}) if !ok { return "", errors.New("unexpected json: config map") } val, ok := cfg[key].(string) if !ok { return "", fmt.Errorf("unexpected json: %s string", key) } return val, nil } type systemTestResourceProvider struct { datastoretest.ResourceProvider } func (rp *systemTestResourceProvider) ValidFn(appID string) *models.Fn { fn := rp.ResourceProvider.ValidFn(appID) fn.Memory = memory fn.Image = image fn.Format = format return fn } var rp = &systemTestResourceProvider{ ResourceProvider: datastoretest.NewBasicResourceProvider(), } func TestCanExecuteFunctionViaTrigger(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() app := ensureApp(t, rp.ValidApp()) fn := ensureFn(t, rp.ValidFn(app.ID)) trigger := ensureTrigger(t, rp.ValidTrigger(app.ID, fn.ID)) 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, "t", app.Name, trigger.Source) body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callTrigger(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 TestCanExecuteTriggerBigOutput(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() app := ensureApp(t, rp.ValidApp()) fn := ensureFn(t, rp.ValidFn(app.ID)) trigger := ensureTrigger(t, rp.ValidTrigger(app.ID, fn.ID)) 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, "t", app.Name, trigger.Source) // Approx 5.3MB output body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true, "trailerRepeat": 410000}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callTrigger(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 TestCanExecuteTriggerTooBigOutput(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() app := ensureApp(t, rp.ValidApp()) fn := ensureFn(t, rp.ValidFn(app.ID)) trigger := ensureTrigger(t, rp.ValidTrigger(app.ID, fn.ID)) 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, "t", app.Name, trigger.Source) // > 6MB output body := `{"echoContent": "HelloWorld", "sleepTime": 0, "isDebug": true, "trailerRepeat": 600000}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callTrigger(ctx, u.String(), content, output, "POST") if err != nil { t.Fatalf("Got unexpected error: %v", err) } exp := "{\"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 TestCanExecuteTriggerEmptyOutput(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() app := ensureApp(t, rp.ValidApp()) fn := ensureFn(t, rp.ValidFn(app.ID)) trigger := ensureTrigger(t, rp.ValidTrigger(app.ID, fn.ID)) 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, "t", app.Name, trigger.Source) // empty body output body := `{"sleepTime": 0, "isDebug": true, "isEmptyBody": true}` content := bytes.NewBuffer([]byte(body)) output := &bytes.Buffer{} resp, err := callTrigger(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 TestBasicTriggerConcurrentExecution(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() app := ensureApp(t, rp.ValidApp()) fn := ensureFn(t, rp.ValidFn(app.ID)) trigger := ensureTrigger(t, rp.ValidTrigger(app.ID, fn.ID)) 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, "t", app.Name, trigger.Source) 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 := callTrigger(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 callTrigger(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 getAPIURL() (string, *url.URL) { apiURL := getEnv("FN_API_URL", "http://localhost:8085") u, err := url.Parse(apiURL) if err != nil { log.Fatalf("Couldn't parse API URL: %s error: %s", apiURL, err) } return apiURL, u } func host() string { u, _ := getAPIURL() return u } const ( appName = "systemtestapp" routeName = "/systemtestroute" image = "fnproject/fn-test-utils" format = "json" memory = 128 typ = "sync" ) func ensureApp(t *testing.T, app *models.App) *models.App { var buf bytes.Buffer err := json.NewEncoder(&buf).Encode(app) if err != nil { t.Fatal("error encoding body", err) } urlStr := host() + "/v2/apps" u, err := url.Parse(urlStr) if err != nil { t.Fatal("error creating url", urlStr, err) } req, err := http.NewRequest("POST", 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()) } var appOut models.App err = json.NewDecoder(&buf).Decode(&appOut) if err != nil { t.Fatal("error decoding response") } return &appOut } func ensureFn(t *testing.T, fn *models.Fn) *models.Fn { var buf bytes.Buffer err := json.NewEncoder(&buf).Encode(fn) if err != nil { t.Fatal("error encoding body", err) } urlStr := host() + "/v2/fns" u, err := url.Parse(urlStr) if err != nil { t.Fatal("error creating url", urlStr, err) } req, err := http.NewRequest("POST", 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()) } var fnOut models.Fn err = json.NewDecoder(&buf).Decode(&fnOut) if err != nil { t.Fatal("error decoding response") } return &fnOut } func ensureTrigger(t *testing.T, trigger *models.Trigger) *models.Trigger { var buf bytes.Buffer err := json.NewEncoder(&buf).Encode(trigger) if err != nil { t.Fatal("error encoding body", err) } urlStr := host() + "/v2/triggers" u, err := url.Parse(urlStr) if err != nil { t.Fatal("error creating url", urlStr, err) } req, err := http.NewRequest("POST", 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()) } var triggerOut models.Trigger err = json.NewDecoder(&buf).Decode(&triggerOut) if err != nil { t.Fatal("error decoding response") } return &triggerOut }