package httpapi import ( "context" "errors" "io" "net" "net/http" "sync" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/fullsync/contract" ) func awaitServer(t *testing.T, done <-chan error) error { t.Helper() select { case err := <-done: return err case <-time.After(5 * time.Second): t.Fatal("server did not stop") return nil } } func startTestServer(t *testing.T, deps Dependencies, timeout time.Duration) (*Server, context.CancelFunc, <-chan error) { t.Helper() server, err := NewServer(Config{Addr: "127.0.0.1:0", BearerToken: testToken, ShutdownTimeout: timeout}, deps) if err != nil { t.Fatal(err) } if server.Addr() != nil { t.Fatal("address before Start") } ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) if err := server.Start(ctx); err != nil { t.Fatal(err) } done := make(chan error, 1) go func() { done <- server.Wait() }() return server, cancel, done } func serverRequest(server *Server) (*http.Response, error) { req, err := http.NewRequest(http.MethodGet, "http://"+server.Addr().String()+runPath, nil) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+testToken) client := &http.Client{Timeout: 4 * time.Second} return client.Do(req) } func TestServerStartServeAndCancel(t *testing.T) { server, cancel, done := startTestServer(t, testDependencies(), 0) response, err := serverRequest(server) if err != nil { t.Fatal(err) } _ = response.Body.Close() if response.StatusCode != 404 { t.Fatalf("status = %d", response.StatusCode) } if err := server.Start(context.Background()); err == nil { t.Fatal("duplicate Start succeeded") } cancel() if err := awaitServer(t, done); err != nil { t.Fatal(err) } if err := server.Wait(); err != nil { t.Fatal(err) } connection, err := net.DialTimeout("tcp", server.Addr().String(), time.Second) if err == nil { _ = connection.Close() t.Fatal("listener still accepts after shutdown") } } func TestServerGracefulShutdownDrainsActiveRequest(t *testing.T) { var ( entered = make(chan struct{}) release = make(chan struct{}) once sync.Once ) t.Cleanup(func() { once.Do(func() { close(release) }) }) deps := testDependencies() deps.Runs = runFunc(func(ctx context.Context, id string) (contract.RunView, error) { close(entered) select { case <-release: return contract.RunView{Source: contract.SourceLive, Run: contract.RunMeta{RunID: id}}, nil case <-ctx.Done(): return contract.RunView{}, ctx.Err() } }) server, cancel, done := startTestServer(t, deps, 2*time.Second) requestDone := make(chan error, 1) go func() { response, err := serverRequest(server) if err == nil { _, err = io.ReadAll(response.Body) _ = response.Body.Close() if response.StatusCode != 200 { err = errors.New("active request did not complete successfully") } } requestDone <- err }() select { case <-entered: case <-time.After(3 * time.Second): t.Fatal("request did not enter handler") } cancel() select { case err := <-done: t.Fatalf("shutdown returned before draining: %v", err) case <-time.After(50 * time.Millisecond): } once.Do(func() { close(release) }) if err := awaitServer(t, requestDone); err != nil { t.Fatal(err) } if err := awaitServer(t, done); err != nil { t.Fatal(err) } } func TestServerShutdownDeadlineClosesActiveConnections(t *testing.T) { var ( entered = make(chan struct{}) exited = make(chan struct{}) ) deps := testDependencies() deps.Runs = runFunc(func(ctx context.Context, _ string) (contract.RunView, error) { close(entered) <-ctx.Done() close(exited) return contract.RunView{}, ctx.Err() }) server, cancel, done := startTestServer(t, deps, 50*time.Millisecond) requestDone := make(chan error, 1) go func() { response, err := serverRequest(server) if response != nil { _ = response.Body.Close() } requestDone <- err }() select { case <-entered: case <-time.After(3 * time.Second): t.Fatal("request did not enter handler") } cancel() if err := awaitServer(t, done); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("shutdown error = %v", err) } select { case <-exited: case <-time.After(3 * time.Second): t.Fatal("forced close did not cancel request context") } _ = awaitServer(t, requestDone) } func TestServerStartupFailureIsSynchronous(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = listener.Close() }) server, err := NewServer(Config{Addr: listener.Addr().String(), BearerToken: testToken}, testDependencies()) if err != nil { t.Fatal(err) } startErr := server.Start(context.Background()) if startErr == nil { t.Fatal("Start did not report occupied port") } if server.Wait() != startErr || server.Addr() != nil { t.Fatal("failed startup state was lost") } var netErr *net.OpError if !errors.As(startErr, &netErr) { t.Fatalf("network error cause lost: %v", startErr) } } func TestServerCanceledBeforeStartAndImmediateCancellation(t *testing.T) { server, err := NewServer(Config{Addr: "127.0.0.1:0", BearerToken: testToken}, testDependencies()) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := server.Start(ctx); !errors.Is(err, context.Canceled) { t.Fatalf("Start = %v", err) } if err := server.Wait(); !errors.Is(err, context.Canceled) { t.Fatalf("Wait = %v", err) } // Cancellation can win the race before Serve starts accepting requests. for i := 0; i < 10; i++ { _, cancel, done := startTestServer(t, testDependencies(), time.Second) cancel() if err := awaitServer(t, done); err != nil { t.Fatal(err) } } } func TestNewServerRejectsInvalidConfiguration(t *testing.T) { for _, config := range []Config{ {BearerToken: testToken}, {Addr: "127.0.0.1:0", BearerToken: testToken, ShutdownTimeout: -time.Second}, } { if _, err := NewServer(config, testDependencies()); err == nil { t.Fatalf("accepted config %+v", config) } } }