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 }