package http_test import ( "context" "io" "net" "net/http" "strings" "testing" "time" httpServer "go.sudomsg.com/kit/http" ) // helper to find free TCP ports func newListener(tb testing.TB) net.Listener { tb.Helper() ln, err := net.Listen("tcp", "localhost:0") if err != nil { tb.Fatalf("failed to open listener: %v", err) } return ln } func TestOpenConfigListeners(t *testing.T) { t.Parallel() t.Run("successful config", func(t *testing.T) { t.Parallel() ctx := t.Context() cfg := []httpServer.ServerConfig{ {Network: "tcp", Address: "localhost:0"}, {Network: "tcp", Address: "localhost:0"}, } lns, err := httpServer.OpenConfigListners(ctx, cfg) if err != nil { t.Fatalf("unexpected error: %v", err) } defer lns.CloseAll() if got := len(lns); got != len(cfg) { t.Errorf("expected %d listeners, got %d", len(cfg), got) } }) t.Run("port conflict triggers cleanup", func(t *testing.T) { t.Parallel() ctx := t.Context() conflict := newListener(t) defer conflict.Close() cfg := []httpServer.ServerConfig{ {Network: "tcp", Address: "localhost:0"}, {Network: "tcp", Address: conflict.Addr().String()}, // will fail } lns, err := httpServer.OpenConfigListners(ctx, cfg) if err == nil { defer lns.CloseAll() t.Fatal("expected error due to conflict, got nil") } }) } func TestCloseAll(t *testing.T) { t.Run("closes all listeners", func(t *testing.T) { t.Parallel() ln1 := newListener(t) ln2 := newListener(t) ls := httpServer.Listeners{ln1, ln2} err := ls.CloseAll() if err != nil { t.Errorf("unexpected error from CloseAll: %v", err) } for _, ln := range ls { if _, err := ln.Accept(); err == nil { t.Error("expected listener to be closed, but Accept succeeded") } } }) } func TestRunHTTPServers(t *testing.T) { t.Parallel() t.Run("basic serve and shutdown", func(t *testing.T) { t.Parallel() ctx := t.Context() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "hello") }) ln := newListener(t) addr := ln.Addr().String() ctx, cancel := context.WithCancel(ctx) defer cancel() go func() { // Wait for server to be ready time.Sleep(200 * time.Millisecond) r, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+addr, nil) resp, err := http.DefaultClient.Do(r) if err != nil { t.Errorf("http.Do error: %v", err) return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if got := strings.TrimSpace(string(body)); got != "hello" { t.Errorf("unexpected response body: %q", got) } cancel() // shutdown the server }() err := httpServer.RunHTTPServers(ctx, httpServer.Listeners{ln}, handler) if err != nil { t.Fatalf("RunHTTPServers failed: %v", err) } }) } func TestInvalidConfig(t *testing.T) { t.Parallel() tests := []struct { name string config httpServer.ServerConfig }{ { name: "invalid network", config: httpServer.ServerConfig{Network: "invalid", Address: "localhost:0"}, }, { name: "invalid address", config: httpServer.ServerConfig{Network: "tcp", Address: "::::"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() ctx := t.Context() _, err := httpServer.OpenConfigListners(ctx, []httpServer.ServerConfig{tt.config}) if err == nil { t.Fatal("OpenConfigListners() expected error, got nil") } }) } }