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 }