knative.dev/func-go@v0.21.3/http/service_test.go (about)

     1  package http
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"testing"
    10  	"time"
    11  
    12  	"knative.dev/func-go/http/mock"
    13  )
    14  
    15  // TestStart_Invoked ensures that the Start method of a function is invoked
    16  // if it is implemented by the function instance.
    17  func TestStart_Invoked(t *testing.T) {
    18  	// Signal to the middleware the Function should be set to listen on
    19  	// an OS-chosen port such that it does not interfere with other services.
    20  	// TODO: this should be an instantiation option such that only mainfiles
    21  	// read and utilize environment variables, and is passed instead to
    22  	// the new service as a functional option
    23  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
    24  
    25  	var (
    26  		ctx, cancel = context.WithCancel(context.Background())
    27  		startCh     = make(chan any)
    28  		errCh       = make(chan error)
    29  		timeoutCh   = time.After(500 * time.Millisecond)
    30  		onStart     = func(_ context.Context, _ map[string]string) error {
    31  			startCh <- true
    32  			return nil
    33  		}
    34  	)
    35  	defer cancel()
    36  
    37  	f := &mock.Function{OnStart: onStart}
    38  
    39  	go func() {
    40  		if err := New(f).Start(ctx); err != nil {
    41  			errCh <- err
    42  		}
    43  	}()
    44  
    45  	select {
    46  	case <-timeoutCh:
    47  		t.Fatal("function failed to notify of start")
    48  	case err := <-errCh:
    49  		t.Fatal(err)
    50  	case <-startCh:
    51  		t.Log("start signal received")
    52  	}
    53  	cancel()
    54  }
    55  
    56  // TestStart_Static checks that static method Start(f) is a convenience method
    57  // for New(f).Start()
    58  func TestStart_Static(t *testing.T) {
    59  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
    60  	var (
    61  		startCh   = make(chan any)
    62  		errCh     = make(chan error)
    63  		timeoutCh = time.After(500 * time.Millisecond)
    64  		onStart   = func(_ context.Context, _ map[string]string) error {
    65  			startCh <- true
    66  			return nil
    67  		}
    68  	)
    69  
    70  	f := &mock.Function{OnStart: onStart}
    71  
    72  	go func() {
    73  		if err := Start(f); err != nil {
    74  			errCh <- err
    75  		}
    76  	}()
    77  
    78  	select {
    79  	case <-timeoutCh:
    80  		t.Fatal("function failed to notify of start")
    81  	case err := <-errCh:
    82  		t.Fatal(err)
    83  	case <-startCh:
    84  		t.Log("start signal received")
    85  	}
    86  }
    87  
    88  // TestStart_CfgEnvs ensures that the function's Start method receives a map
    89  // containing all available environment variables as a parameter.
    90  //
    91  // All environment variables are stored in a map which becomes the
    92  // single argument 'cfg' passed to the Function's Start method.  This ensures
    93  // that Functions can run in any context and are not coupled to os environment
    94  // variables.
    95  func TestStart_CfgEnvs(t *testing.T) {
    96  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
    97  	var (
    98  		ctx, cancel = context.WithCancel(context.Background())
    99  		startCh     = make(chan any)
   100  		errCh       = make(chan error)
   101  		timeoutCh   = time.After(500 * time.Millisecond)
   102  		onStart     = func(_ context.Context, cfg map[string]string) error {
   103  			v := cfg["TEST_ENV"]
   104  			if v != "example_value" {
   105  				t.Fatalf("did not receive TEST_ENV.  got %v", cfg["TEST_ENV"])
   106  			} else {
   107  				t.Log("expected value received")
   108  			}
   109  			startCh <- true
   110  			return nil
   111  		}
   112  	)
   113  	defer cancel()
   114  
   115  	f := &mock.Function{OnStart: onStart}
   116  
   117  	t.Setenv("TEST_ENV", "example_value")
   118  
   119  	go func() {
   120  		if err := New(f).Start(ctx); err != nil {
   121  			errCh <- err
   122  		}
   123  	}()
   124  
   125  	select {
   126  	case <-timeoutCh:
   127  		t.Fatal("function failed to notify of start")
   128  	case err := <-errCh:
   129  		t.Fatal(err)
   130  	case <-startCh:
   131  		t.Log("start signal received")
   132  	}
   133  }
   134  
   135  // TestStart_CfgStatic ensures that additional static "environment variables"
   136  // built into the container as cfg.  The format is one variable per line,
   137  // [key]=[value].
   138  //
   139  // This file is used by `func` to build metadata about a function for use
   140  // at runtime such as the function's version (if using git), the version of
   141  // func used to scaffold the function, etc.
   142  func TestCfg_Static(t *testing.T) {
   143  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
   144  	var (
   145  		ctx, cancel = context.WithCancel(context.Background())
   146  		startCh     = make(chan any)
   147  		errCh       = make(chan error)
   148  		timeoutCh   = time.After(500 * time.Millisecond)
   149  	)
   150  	defer cancel()
   151  
   152  	// Run test from within a temp dir
   153  	dir := t.TempDir()
   154  	if err := os.Chdir(dir); err != nil {
   155  		t.Fatal(err)
   156  	}
   157  
   158  	// Write an example `cfg` file
   159  	if err := os.WriteFile("cfg", []byte(`FUNC_VERSION="v1.2.3"`), os.ModePerm); err != nil {
   160  		t.Fatal(err)
   161  	}
   162  
   163  	// Function which verifies it received the value
   164  	f := &mock.Function{OnStart: func(_ context.Context, cfg map[string]string) error {
   165  		v := cfg["FUNC_VERSION"]
   166  		if v != "v1.2.3" {
   167  			t.Fatalf("FUNC_VERSION not received.  Expected 'v1.2.3', got '%v'",
   168  				cfg["FUNC_VERSION"])
   169  
   170  		} else {
   171  			t.Log("expected value received")
   172  		}
   173  		startCh <- true
   174  		return nil
   175  	}}
   176  
   177  	// Run the function
   178  	go func() {
   179  		if err := New(f).Start(ctx); err != nil {
   180  			errCh <- err
   181  		}
   182  	}()
   183  
   184  	// Wait for a signal the onStart indicatig the function executed
   185  	select {
   186  	case <-timeoutCh:
   187  		t.Fatal("function failed to notify of start")
   188  	case err := <-errCh:
   189  		t.Fatal(err)
   190  	case <-startCh:
   191  		t.Log("start signal received")
   192  	}
   193  }
   194  
   195  // TestStop_Invoked ensures the Stop method of a function is invoked on context
   196  // cancellation if it is implemented by the function instance.
   197  func TestStop_Invoked(t *testing.T) {
   198  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
   199  	var (
   200  		ctx, cancel = context.WithCancel(context.Background())
   201  		startCh     = make(chan any)
   202  		stopCh      = make(chan any)
   203  		errCh       = make(chan error)
   204  		timeoutCh   = time.After(500 * time.Millisecond)
   205  		onStart     = func(_ context.Context, _ map[string]string) error {
   206  			startCh <- true
   207  			return nil
   208  		}
   209  		onStop = func(_ context.Context) error {
   210  			stopCh <- true
   211  			return nil
   212  		}
   213  	)
   214  
   215  	f := &mock.Function{OnStart: onStart, OnStop: onStop}
   216  
   217  	go func() {
   218  		if err := New(f).Start(ctx); err != nil {
   219  			errCh <- err
   220  		}
   221  	}()
   222  
   223  	// Wait for start, error starting or hang
   224  	select {
   225  	case <-timeoutCh:
   226  		t.Fatal("function failed to notify of start")
   227  	case err := <-errCh:
   228  		t.Fatal(err)
   229  	case <-startCh:
   230  		t.Log("start signal received")
   231  	}
   232  
   233  	// Cancel the context (trigger a stop)
   234  	cancel()
   235  
   236  	// Wait for stop signal, error stopping, or hang
   237  	select {
   238  	case <-time.After(500 * time.Millisecond):
   239  		t.Fatal("function failed to notify of stop")
   240  	case err := <-errCh:
   241  		t.Fatal(err)
   242  	case <-stopCh:
   243  		t.Log("stop signal received")
   244  	}
   245  }
   246  
   247  // TestHandle_Invoked ensures the Handle method of a function is invoked on
   248  // a successful http request.
   249  func TestHandle_Invoked(t *testing.T) {
   250  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
   251  	var (
   252  		ctx, cancel = context.WithCancel(context.Background())
   253  		errCh       = make(chan error)
   254  		startCh     = make(chan any)
   255  		timeoutCh   = time.After(500 * time.Millisecond)
   256  		onStart     = func(_ context.Context, _ map[string]string) error {
   257  			startCh <- true
   258  			return nil
   259  		}
   260  		onHandle = func(w http.ResponseWriter, _ *http.Request) {
   261  			fmt.Fprintf(w, "OK")
   262  		}
   263  	)
   264  	defer cancel()
   265  
   266  	f := &mock.Function{OnStart: onStart, OnHandle: onHandle}
   267  	service := New(f)
   268  
   269  	go func() {
   270  		if err := service.Start(ctx); err != nil {
   271  			errCh <- err
   272  		}
   273  	}()
   274  
   275  	select {
   276  	case <-timeoutCh:
   277  		t.Fatal("function failed to start")
   278  	case err := <-errCh:
   279  		t.Fatal(err)
   280  	case <-startCh:
   281  	}
   282  
   283  	t.Logf("Service address: %v\n", service.Addr())
   284  
   285  	resp, err := http.Get("http://" + service.Addr().String())
   286  	if err != nil {
   287  		t.Fatal(err)
   288  	}
   289  	defer resp.Body.Close()
   290  
   291  	if resp.StatusCode != http.StatusOK {
   292  		t.Fatalf("unexpected http status code: %v", resp.StatusCode)
   293  	}
   294  	body, err := io.ReadAll(resp.Body)
   295  	if err != nil {
   296  		t.Fatal(err)
   297  	}
   298  	if string(body) != "OK" {
   299  		t.Fatalf("unexpected body: %v\n", string(body))
   300  	}
   301  
   302  }
   303  
   304  // TestReady_Invoked ensures the default Ready Handle method of a function is invoked on
   305  // a successful http request.
   306  func TestReady_Invoked(t *testing.T) {
   307  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
   308  
   309  	var (
   310  		ctx, cancel = context.WithCancel(context.Background())
   311  		errCh       = make(chan error)
   312  		startCh     = make(chan any)
   313  		timeoutCh   = time.After(500 * time.Millisecond)
   314  		onStart     = func(_ context.Context, _ map[string]string) error {
   315  			startCh <- true
   316  			return nil
   317  		}
   318  	)
   319  	defer cancel()
   320  
   321  	f := &mock.Function{OnStart: onStart}
   322  	service := New(f)
   323  	go func() {
   324  		if err := service.Start(ctx); err != nil {
   325  			errCh <- err
   326  		}
   327  	}()
   328  
   329  	select {
   330  	case <-timeoutCh:
   331  		t.Fatal("Service timed out")
   332  	case err := <-errCh:
   333  		t.Fatal(err)
   334  	case <-startCh:
   335  		// Service started successfully
   336  	}
   337  
   338  	t.Logf("Service address: %v\n", service.Addr())
   339  
   340  	resp, err := http.Get("http://" + service.Addr().String() + "/health/readiness")
   341  	if err != nil {
   342  		t.Fatal(err)
   343  	}
   344  	defer resp.Body.Close()
   345  
   346  	if resp.StatusCode != http.StatusOK {
   347  		t.Fatalf("unexpected http status code: %v", resp.StatusCode)
   348  	}
   349  }
   350  
   351  // TestAlive_Invoked ensures the default Alive Handle method of a function is invoked on
   352  // a successful http request.
   353  func TestAlive_Invoked(t *testing.T) {
   354  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
   355  
   356  	var (
   357  		ctx, cancel = context.WithCancel(context.Background())
   358  		errCh       = make(chan error)
   359  		startCh     = make(chan any)
   360  		timeoutCh   = time.After(500 * time.Millisecond)
   361  		onStart     = func(context.Context, map[string]string) error {
   362  			startCh <- true
   363  			return nil
   364  		}
   365  	)
   366  	defer cancel()
   367  
   368  	f := &mock.Function{OnStart: onStart}
   369  	service := New(f)
   370  	go func() {
   371  		if err := service.Start(ctx); err != nil {
   372  			errCh <- err
   373  		}
   374  	}()
   375  
   376  	select {
   377  	case <-timeoutCh:
   378  		t.Fatal("Service timed out")
   379  	case err := <-errCh:
   380  		t.Fatal(err)
   381  	case <-startCh:
   382  		// Service started successfully
   383  	}
   384  
   385  	t.Logf("Service address: %v\n", service.Addr())
   386  
   387  	resp, err := http.Get("http://" + service.Addr().String() + "/health/liveness")
   388  	if err != nil {
   389  		t.Fatal(err)
   390  	}
   391  	defer resp.Body.Close()
   392  
   393  	if resp.StatusCode != http.StatusOK {
   394  		t.Fatalf("unexpected http status code: %v", resp.StatusCode)
   395  	}
   396  }
   397  
   398  // TestHandle_WithContext ensures that the system allows compilation of
   399  // functions provided with the legacy Handler method signature.
   400  //
   401  // This compatibility layer can be removed once func-go has been integrated
   402  // into the Buildpack and S2I builders.
   403  func TestHandle_WithContext(t *testing.T) {
   404  	// This is the handler with the deprecated method signature:
   405  	deprecatedHandler := func(_ context.Context, w http.ResponseWriter, _ *http.Request) {
   406  		fmt.Fprintf(w, "OK")
   407  	}
   408  
   409  	// This is how the scaffolding middleware provides a static function
   410  	// handler to the func-go middleware.  This used to only support
   411  	// values of type Handler, but has been temporarily loosened to support
   412  	// both Handler and HandlerWithContext
   413  	f := DefaultHandler{Handler: deprecatedHandler}
   414  
   415  	// If the following fails to compile, it's definitely not working.
   416  	// The tests in func (which relies on this backwards compatibility)
   417  	// will do a full test.
   418  	go func() {
   419  		_ = Start(f)
   420  	}()
   421  	t.Log("legacy static handler signature accepted.  see func tests for confirmation of invocation")
   422  }