~bigbes/sr-ht-ecore

ref: 3aa1fcfd9a55d3a42f0e8e3f5ca52725f9a2b2a6 sr-ht-ecore/mcphttp/cache_test.go -rw-r--r-- 5.6 KiB
3aa1fcfd — Eugene Blikh mcphttp: the MCP endpoint plumbing both services already share 2 days ago
                                                                                
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package mcphttp_test

import (
	"bufio"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"sourcecraft.dev/bigbes/sr-ht-ecore/mcphttp"
)

const (
	wantCacheControl = "private, no-store, no-transform"
	wantVary         = "Cookie, Authorization"
)

// TestHeadersLandOnAnExplicitWriteHeader is the ordinary path: a handler that
// commits with WriteHeader.
func TestHeadersLandOnAnExplicitWriteHeader(t *testing.T) {
	h := mcphttp.PrivateCache(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusAccepted)
	}))

	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/mcp", nil))

	assert.Equal(t, http.StatusAccepted, rec.Code)
	assert.Equal(t, wantCacheControl, rec.Header().Get("Cache-Control"))
	assert.Equal(t, wantVary, rec.Header().Get("Vary"))
}

// TestHeadersLandOnAnImplicitCommit covers the handler that never calls
// WriteHeader at all. net/http commits on the first Write, and headers set after
// that point are dropped silently — so the wrapper has to catch Write too.
func TestHeadersLandOnAnImplicitCommit(t *testing.T) {
	h := mcphttp.PrivateCache(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte(`{"jsonrpc":"2.0"}`))
	}))

	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/mcp", nil))

	assert.Equal(t, http.StatusOK, rec.Code)
	assert.Equal(t, wantCacheControl, rec.Header().Get("Cache-Control"))
	assert.Equal(t, wantVary, rec.Header().Get("Vary"))
	assert.Equal(t, `{"jsonrpc":"2.0"}`, rec.Body.String())
}

// TestTheSDKsOwnDirectivesAreOverridden is the reason this wrapper exists rather
// than middleware.PrivateCache. The streamable transport sets Cache-Control with
// Set from inside the handler, so anything written on the way in loses. What
// arrives here is a handler doing exactly what the SDK does.
func TestTheSDKsOwnDirectivesAreOverridden(t *testing.T) {
	h := mcphttp.PrivateCache(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.Header().Set("Cache-Control", "no-cache, no-transform")
		w.WriteHeader(http.StatusOK)
	}))

	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/mcp", nil))

	assert.Equal(t, wantCacheControl, rec.Header().Get("Cache-Control"),
		"no-cache permits a cache to store the body and revalidate, which is what no-store forbids")
	assert.Equal(t, wantVary, rec.Header().Get("Vary"))
}

// TestOnlyTheFirstCommitWrites checks the committed flag: a handler that writes
// its own header after committing is doing something net/http would ignore
// anyway, and the wrapper must not undo a status that is already on the wire by
// re-running its Set on every Write.
func TestOnlyTheFirstCommitWrites(t *testing.T) {
	h := mcphttp.PrivateCache(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("a"))
		_, _ = w.Write([]byte("b"))
	}))

	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/mcp", nil))

	assert.Equal(t, []string{wantCacheControl}, rec.Header().Values("Cache-Control"))
	assert.Equal(t, []string{wantVary}, rec.Header().Values("Vary"))
	assert.Equal(t, "ab", rec.Body.String())
}

// TestUnwrapReachesTheUnderlyingFlusher is the test the Unwrap method exists
// for.
//
// cacheWriter embeds the http.ResponseWriter *interface*, so it promotes no
// Flush of its own; http.NewResponseController can only reach the real writer's
// through Unwrap. Delete the method and this fails with ErrNotSupported while
// every status-code and header test above stays green — which is precisely why
// it is written as a test and not as a comment.
func TestUnwrapReachesTheUnderlyingFlusher(t *testing.T) {
	var flushErr error
	h := mcphttp.PrivateCache(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = w.Write([]byte("event: message\n"))
		flushErr = http.NewResponseController(w).Flush()
	}))

	rec := httptest.NewRecorder()
	h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/mcp", nil))

	require.NoError(t, flushErr, "an SSE stream that cannot be flushed is a response no client sees")
	assert.True(t, rec.Flushed)
	assert.Equal(t, wantCacheControl, rec.Header().Get("Cache-Control"))
}

// TestAStreamReachesTheClientBeforeTheHandlerReturns is the same property
// measured rather than asserted: over a real connection, with a real client,
// bytes written and flushed inside the handler have to arrive while the handler
// is still running. A recorder cannot tell a flush that worked from one that was
// buffered until the end.
func TestAStreamReachesTheClientBeforeTheHandlerReturns(t *testing.T) {
	release := make(chan struct{})
	srv := httptest.NewServer(mcphttp.PrivateCache(http.HandlerFunc(
		func(w http.ResponseWriter, _ *http.Request) {
			w.Header().Set("Content-Type", "text/event-stream")
			_, _ = w.Write([]byte("data: first\n\n"))
			if err := http.NewResponseController(w).Flush(); err != nil {
				t.Errorf("flush: %v", err)
			}
			<-release
		})))
	t.Cleanup(func() {
		close(release)
		srv.Close()
	})

	req, err := http.NewRequest(http.MethodGet, srv.URL, nil)
	require.NoError(t, err)
	resp, err := (&http.Client{Timeout: 5 * time.Second}).Do(req)
	require.NoError(t, err)
	t.Cleanup(func() { _ = resp.Body.Close() })

	assert.Equal(t, wantCacheControl, resp.Header.Get("Cache-Control"))
	assert.Equal(t, wantVary, resp.Header.Get("Vary"))

	line, err := bufio.NewReader(resp.Body).ReadString('\n')
	require.NoError(t, err, "the first event must arrive while the handler is still blocked")
	assert.Equal(t, "data: first\n", line)
}