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

     1  package cloudevents
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"os"
     9  	"testing"
    10  	"time"
    11  
    12  	cloudevents "github.com/cloudevents/sdk-go/v2"
    13  	"github.com/cloudevents/sdk-go/v2/event"
    14  	"knative.dev/func-go/cloudevents/mock"
    15  )
    16  
    17  // TestStart_Invoked ensures that the Start method of a function is invoked
    18  // if it is implemented by the function instance.
    19  func TestStart_Invoked(t *testing.T) {
    20  	var (
    21  		ctx, cancel = context.WithCancel(context.Background())
    22  		startCh     = make(chan any)
    23  		errCh       = make(chan error)
    24  		timeoutCh   = time.After(500 * time.Millisecond)
    25  		onStart     = func(_ context.Context, _ map[string]string) error {
    26  			startCh <- true
    27  			return nil
    28  		}
    29  	)
    30  	defer cancel()
    31  
    32  	f := &mock.Function{OnStart: onStart}
    33  
    34  	go func() {
    35  		if err := New(f).Start(ctx); err != nil {
    36  			errCh <- err
    37  		}
    38  	}()
    39  
    40  	select {
    41  	case <-timeoutCh:
    42  		t.Fatal("function failed to notify of start")
    43  	case err := <-errCh:
    44  		t.Fatal(err)
    45  	case <-startCh:
    46  		t.Log("start signal received")
    47  	}
    48  	cancel()
    49  }
    50  
    51  // TestStart_Static checks that static method Start(f) is a convenience method
    52  // for New(f).Start()
    53  func TestStart_Static(t *testing.T) {
    54  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
    55  	var (
    56  		startCh   = make(chan any)
    57  		errCh     = make(chan error)
    58  		timeoutCh = time.After(500 * time.Millisecond)
    59  		onStart   = func(_ context.Context, _ map[string]string) error {
    60  			startCh <- true
    61  			return nil
    62  		}
    63  	)
    64  
    65  	f := &mock.Function{OnStart: onStart}
    66  
    67  	go func() {
    68  		if err := Start(f); err != nil {
    69  			errCh <- err
    70  		}
    71  	}()
    72  
    73  	select {
    74  	case <-timeoutCh:
    75  		t.Fatal("function failed to notify of start")
    76  	case err := <-errCh:
    77  		t.Fatal(err)
    78  	case <-startCh:
    79  		t.Log("start signal received")
    80  	}
    81  }
    82  
    83  // TestStart_CfgEnvs ensures that the function's Start method receives a map
    84  // containing all available environment variables as a parameter.
    85  //
    86  // All environment variables are stored in a map which becomes the
    87  // single argument 'cfg' passed to the Function's Start method.  This ensures
    88  // that Functions can run in any context and are not coupled to os environment
    89  // variables.
    90  func TestStart_CfgEnvs(t *testing.T) {
    91  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
    92  	var (
    93  		ctx, cancel = context.WithCancel(context.Background())
    94  		startCh     = make(chan any)
    95  		errCh       = make(chan error)
    96  		timeoutCh   = time.After(500 * time.Millisecond)
    97  		onStart     = func(_ context.Context, cfg map[string]string) error {
    98  			v := cfg["TEST_ENV"]
    99  			if v != "example_value" {
   100  				t.Fatalf("did not receive TEST_ENV.  got %v", cfg["TEST_ENV"])
   101  			} else {
   102  				t.Log("expected value received")
   103  			}
   104  			startCh <- true
   105  			return nil
   106  		}
   107  	)
   108  	defer cancel()
   109  
   110  	f := &mock.Function{OnStart: onStart}
   111  
   112  	t.Setenv("TEST_ENV", "example_value")
   113  
   114  	go func() {
   115  		if err := New(f).Start(ctx); err != nil {
   116  			errCh <- err
   117  		}
   118  	}()
   119  
   120  	select {
   121  	case <-timeoutCh:
   122  		t.Fatal("function failed to notify of start")
   123  	case err := <-errCh:
   124  		t.Fatal(err)
   125  	case <-startCh:
   126  		t.Log("start signal received")
   127  	}
   128  }
   129  
   130  // TestStart_CfgStatic ensures that additional static "environment variables"
   131  // built into the container as cfg.  The format is one variable per line,
   132  // [key]=[value].
   133  //
   134  // This file is used by `func` to build metadata about a function for use
   135  // at runtime such as the function's version (if using git), the version of
   136  // func used to scaffold the function, etc.
   137  func TestCfg_Static(t *testing.T) {
   138  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
   139  	var (
   140  		ctx, cancel = context.WithCancel(context.Background())
   141  		startCh     = make(chan any)
   142  		errCh       = make(chan error)
   143  		timeoutCh   = time.After(500 * time.Millisecond)
   144  	)
   145  	defer cancel()
   146  
   147  	// Run test from within a temp dir
   148  	dir := t.TempDir()
   149  	if err := os.Chdir(dir); err != nil {
   150  		t.Fatal(err)
   151  	}
   152  
   153  	// Write an example `cfg` file
   154  	if err := os.WriteFile("cfg", []byte(`FUNC_VERSION="v1.2.3"`), os.ModePerm); err != nil {
   155  		t.Fatal(err)
   156  	}
   157  
   158  	// Function which verifies it received the value
   159  	f := &mock.Function{OnStart: func(_ context.Context, cfg map[string]string) error {
   160  		v := cfg["FUNC_VERSION"]
   161  		if v != "v1.2.3" {
   162  			t.Fatalf("FUNC_VERSION not received.  Expected 'v1.2.3', got '%v'",
   163  				cfg["FUNC_VERSION"])
   164  
   165  		} else {
   166  			t.Log("expected value received")
   167  		}
   168  		startCh <- true
   169  		return nil
   170  	}}
   171  
   172  	// Run the function
   173  	go func() {
   174  		if err := New(f).Start(ctx); err != nil {
   175  			errCh <- err
   176  		}
   177  	}()
   178  
   179  	// Wait for a signal the onStart indicatig the function executed
   180  	select {
   181  	case <-timeoutCh:
   182  		t.Fatal("function failed to notify of start")
   183  	case err := <-errCh:
   184  		t.Fatal(err)
   185  	case <-startCh:
   186  		t.Log("start signal received")
   187  	}
   188  }
   189  
   190  // TestStop_Invoked ensures the Stop method of a function is invoked on context
   191  // cancellation if it is implemented by the function instance.
   192  func TestStop_Invoked(t *testing.T) {
   193  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
   194  	var (
   195  		ctx, cancel = context.WithCancel(context.Background())
   196  		startCh     = make(chan any)
   197  		stopCh      = make(chan any)
   198  		errCh       = make(chan error)
   199  		timeoutCh   = time.After(500 * time.Millisecond)
   200  		onStart     = func(_ context.Context, _ map[string]string) error {
   201  			startCh <- true
   202  			return nil
   203  		}
   204  		onStop = func(_ context.Context) error {
   205  			stopCh <- true
   206  			return nil
   207  		}
   208  	)
   209  
   210  	f := &mock.Function{OnStart: onStart, OnStop: onStop}
   211  
   212  	go func() {
   213  		if err := New(f).Start(ctx); err != nil {
   214  			errCh <- err
   215  		}
   216  	}()
   217  
   218  	// Wait for start, error starting or hang
   219  	select {
   220  	case <-timeoutCh:
   221  		t.Fatal("function failed to notify of start")
   222  	case err := <-errCh:
   223  		t.Fatal(err)
   224  	case <-startCh:
   225  		t.Log("start signal received")
   226  	}
   227  
   228  	// Cancel the context (trigger a stop)
   229  	cancel()
   230  
   231  	// Wait for stop signal, error stopping, or hang
   232  	select {
   233  	case <-time.After(500 * time.Millisecond):
   234  		t.Fatal("function failed to notify of stop")
   235  	case err := <-errCh:
   236  		t.Fatal(err)
   237  	case <-stopCh:
   238  		t.Log("stop signal received")
   239  	}
   240  }
   241  
   242  // TestHandle_Invoked ensures the Handle method of a function is invoked on
   243  // a successful http request.
   244  func TestHandle_Invoked(t *testing.T) {
   245  	t.Setenv("LISTEN_ADDRESS", "127.0.0.1:") // use an OS-chosen port
   246  
   247  	var (
   248  		ctx, cancel = context.WithCancel(context.Background())
   249  		errCh       = make(chan error)
   250  		startCh     = make(chan any)
   251  		timeoutCh   = time.After(500 * time.Millisecond)
   252  		onStart     = func(_ context.Context, _ map[string]string) error {
   253  			startCh <- true
   254  			return nil
   255  		}
   256  		onHandle = func(_ context.Context, event event.Event) (*event.Event, error) {
   257  			fmt.Println("Instanced CloudEvents handler invoked")
   258  			fmt.Println(event) // echo to local output
   259  			return nil, nil    // echo to caller
   260  		}
   261  	)
   262  	defer cancel()
   263  
   264  	f := &mock.Function{OnStart: onStart, OnHandle: onHandle}
   265  	service := New(f)
   266  
   267  	go func() {
   268  		if err := service.Start(ctx); err != nil {
   269  			errCh <- err
   270  		}
   271  	}()
   272  
   273  	select {
   274  	case <-timeoutCh:
   275  		t.Fatal("function failed to start")
   276  	case err := <-errCh:
   277  		t.Fatal(err)
   278  	case <-startCh:
   279  	}
   280  
   281  	t.Logf("Service address: %v\n", service.Addr())
   282  
   283  	// Send a request:
   284  	c, err := cloudevents.NewClientHTTP()
   285  	if err != nil {
   286  		log.Fatalf("failed to create client, %v", err)
   287  	}
   288  
   289  	// Create an Event.
   290  	event := cloudevents.NewEvent()
   291  	event.SetSource("example/uri")
   292  	event.SetType("example.type")
   293  	event.SetData(cloudevents.ApplicationJSON, map[string]string{"hello": "world"})
   294  
   295  	// Set a target.
   296  	ctx = cloudevents.ContextWithTarget(context.Background(), "http://"+service.Addr().String())
   297  
   298  	// Send that Event.
   299  	if result := c.Send(ctx, event); cloudevents.IsUndelivered(result) {
   300  		log.Fatalf("failed to send, %v", result)
   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  }