summaryrefslogtreecommitdiff
path: root/internal/cli/serve_test.go
blob: 6bd0e8f667757a50aa9fbfcc9d43d9ee626509c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package cli

import (
	"context"
	"log/slog"
	"net"
	"net/http"
	"sync"
	"testing"
	"time"
)

// recordHandler captures log records for assertions.
type recordHandler struct {
	mu      sync.Mutex
	records []slog.Record
}

func (h *recordHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
func (h *recordHandler) Handle(_ context.Context, r slog.Record) error {
	h.mu.Lock()
	h.records = append(h.records, r)
	h.mu.Unlock()
	return nil
}
func (h *recordHandler) WithAttrs(_ []slog.Attr) slog.Handler  { return h }
func (h *recordHandler) WithGroup(_ string) slog.Handler       { return h }
func (h *recordHandler) hasWarn(msg string) bool {
	h.mu.Lock()
	defer h.mu.Unlock()
	for _, r := range h.records {
		if r.Level == slog.LevelWarn && r.Message == msg {
			return true
		}
	}
	return false
}

// TestServe_ShutdownError_IsLogged verifies that a shutdown timeout error is
// logged as a warning rather than silently dropped.
func TestServe_ShutdownError_IsLogged(t *testing.T) {
	// Start a real listener so we have an address.
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen: %v", err)
	}

	// Handler that hangs so the active connection prevents clean shutdown.
	hang := make(chan struct{})
	mux := http.NewServeMux()
	mux.HandleFunc("/hang", func(w http.ResponseWriter, r *http.Request) {
		<-hang
	})

	srv := &http.Server{Handler: mux}

	// Serve in background.
	go srv.Serve(ln) //nolint:errcheck

	// Open a connection and start a hanging request so the server has an
	// active connection when we call Shutdown.
	addr := ln.Addr().String()
	connReady := make(chan struct{})
	go func() {
		req, _ := http.NewRequest(http.MethodGet, "http://"+addr+"/hang", nil)
		close(connReady)
		http.DefaultClient.Do(req) //nolint:errcheck
	}()
	<-connReady
	// Give the goroutine a moment to establish the request.
	time.Sleep(20 * time.Millisecond)

	// Shutdown with an already-expired deadline so it times out immediately.
	expiredCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Second))
	defer cancel()

	h := &recordHandler{}
	logger := slog.New(h)

	// This is the exact logic from serve.go's shutdown goroutine.
	if err := srv.Shutdown(expiredCtx); err != nil {
		logger.Warn("shutdown error", "err", err)
	}

	// Unblock the hanging handler.
	close(hang)

	if !h.hasWarn("shutdown error") {
		t.Error("expected shutdown error to be logged as Warn, but it was not")
	}
}