@@ -0,0 +1,94 @@
1+package server
2+
3+import (
4+ "encoding/json"
5+ "io"
6+ "net/http"
7+ "net/http/httptest"
8+ "testing"
9+
10+ "github.com/charmbracelet/crush/internal/proto"
11+ "github.com/stretchr/testify/require"
12+)
13+
14+// TestRecoverHandler_PanicReturns500 verifies that a panicking handler
15+// surfaces as a structured 500 to the client, rather than closing the
16+// connection silently and producing an opaque EOF.
17+func TestRecoverHandler_PanicReturns500(t *testing.T) {
18+ t.Parallel()
19+
20+ s := &Server{}
21+ h := s.recoverHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22+ panic("kaboom")
23+ }))
24+
25+ rec := httptest.NewRecorder()
26+ req := httptest.NewRequest(http.MethodGet, "/test", nil)
27+ h.ServeHTTP(rec, req)
28+
29+ require.Equal(t, http.StatusInternalServerError, rec.Code)
30+ body, err := io.ReadAll(rec.Body)
31+ require.NoError(t, err)
32+ require.NotEmpty(t, body)
33+
34+ var perr proto.Error
35+ require.NoError(t, json.Unmarshal(body, &perr))
36+ require.NotEmpty(t, perr.Message)
37+}
38+
39+// TestRecoverHandler_NoPanicPassthrough verifies that the middleware
40+// does not interfere with successful responses.
41+func TestRecoverHandler_NoPanicPassthrough(t *testing.T) {
42+ t.Parallel()
43+
44+ s := &Server{}
45+ h := s.recoverHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46+ w.WriteHeader(http.StatusTeapot)
47+ _, _ = w.Write([]byte("ok"))
48+ }))
49+
50+ rec := httptest.NewRecorder()
51+ req := httptest.NewRequest(http.MethodGet, "/test", nil)
52+ h.ServeHTTP(rec, req)
53+
54+ require.Equal(t, http.StatusTeapot, rec.Code)
55+ require.Equal(t, "ok", rec.Body.String())
56+}
57+
58+// TestRecoverHandler_PanicAfterWriteHeader verifies that if a handler
59+// panics after it has already started writing the response, the
60+// middleware does not attempt to overwrite the status (which would
61+// trigger a superfluous WriteHeader warning) but still logs and
62+// recovers.
63+func TestRecoverHandler_PanicAfterWriteHeader(t *testing.T) {
64+ t.Parallel()
65+
66+ s := &Server{}
67+ h := s.recoverHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
68+ w.WriteHeader(http.StatusOK)
69+ _, _ = w.Write([]byte("partial"))
70+ panic("late panic")
71+ }))
72+
73+ rec := httptest.NewRecorder()
74+ req := httptest.NewRequest(http.MethodGet, "/test", nil)
75+ require.NotPanics(t, func() { h.ServeHTTP(rec, req) })
76+ require.Equal(t, http.StatusOK, rec.Code)
77+ require.Equal(t, "partial", rec.Body.String())
78+}
79+
80+// TestRecoverHandler_AbortHandlerPropagates verifies that the documented
81+// http.ErrAbortHandler sentinel is re-panicked so the net/http server
82+// can handle it normally (suppress logging, close connection).
83+func TestRecoverHandler_AbortHandlerPropagates(t *testing.T) {
84+ t.Parallel()
85+
86+ s := &Server{}
87+ h := s.recoverHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
88+ panic(http.ErrAbortHandler)
89+ }))
90+
91+ rec := httptest.NewRecorder()
92+ req := httptest.NewRequest(http.MethodGet, "/test", nil)
93+ require.PanicsWithValue(t, http.ErrAbortHandler, func() { h.ServeHTTP(rec, req) })
94+}