Skip to content

Commit 05c46bb

Browse files
committed
test(tunnel): cover stream race handling (2026.6.17.0-F138)
1 parent f8fd489 commit 05c46bb

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

internal/capabilities/tunnel_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
package capabilities
33

44
import (
5+
"bytes"
56
"context"
67
"encoding/base64"
78
"errors"
89
"io"
910
"net"
11+
"net/http"
12+
"net/http/httptest"
13+
"net/url"
1014
"strconv"
1115
"sync"
1216
"testing"
@@ -80,6 +84,31 @@ func startEchoServer(t *testing.T) (int, func()) {
8084
return port, func() { _ = listener.Close() }
8185
}
8286

87+
func startHTTPServer(t *testing.T) (int, func()) {
88+
t.Helper()
89+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
90+
w.Header().Set("Content-Type", "text/plain")
91+
w.Header().Set("Connection", "close")
92+
_, _ = w.Write([]byte("fritz ok"))
93+
}))
94+
u, err := url.Parse(server.URL)
95+
if err != nil {
96+
server.Close()
97+
t.Fatalf("parse server URL: %v", err)
98+
}
99+
_, portText, err := net.SplitHostPort(u.Host)
100+
if err != nil {
101+
server.Close()
102+
t.Fatalf("split server host: %v", err)
103+
}
104+
port, err := strconv.Atoi(portText)
105+
if err != nil {
106+
server.Close()
107+
t.Fatalf("parse server port: %v", err)
108+
}
109+
return port, server.Close
110+
}
111+
83112
func TestTunnelManagerOpensAndForwardsBytes(t *testing.T) {
84113
withSessionRiskPrompt(t, func(context.Context, riskRequest) (bool, error) { return true, nil })
85114
port, stop := startEchoServer(t)
@@ -156,6 +185,73 @@ func TestTunnelManagerWaitsForStreamBeforeWritingData(t *testing.T) {
156185
}
157186
}
158187

188+
func TestTunnelManagerHTTPGetCanRaceStreamOpen(t *testing.T) {
189+
port, stop := startHTTPServer(t)
190+
defer stop()
191+
192+
bridge := newFakeTunnelBridge()
193+
mgr := newTunnelMgr(bridge)
194+
defer mgr.shutdown()
195+
196+
if _, err := mgr.open("tn1", port, "127.0.0.1"); err != nil {
197+
t.Fatalf("open: %v", err)
198+
}
199+
200+
request := []byte("GET /login_sid.lua HTTP/1.1\r\nHost: fritz.repeater\r\nConnection: close\r\n\r\n")
201+
writeDone := make(chan error, 1)
202+
go func() {
203+
writeDone <- mgr.writeData("tn1", "s1", request)
204+
}()
205+
206+
time.Sleep(50 * time.Millisecond)
207+
if err := mgr.openStream(context.Background(), "tn1", "s1"); err != nil {
208+
t.Fatalf("openStream: %v", err)
209+
}
210+
if err := <-writeDone; err != nil {
211+
t.Fatalf("writeData: %v", err)
212+
}
213+
214+
var response []byte
215+
deadline := time.After(2 * time.Second)
216+
for !bytes.Contains(response, []byte("fritz ok")) {
217+
select {
218+
case rec := <-bridge.dataCh:
219+
response = append(response, rec.payload...)
220+
case <-deadline:
221+
t.Fatalf("HTTP response did not arrive; got %q", response)
222+
}
223+
}
224+
}
225+
226+
func TestTunnelManagerWriteDataStopsWaitingWhenTunnelCloses(t *testing.T) {
227+
bridge := newFakeTunnelBridge()
228+
mgr := newTunnelMgr(bridge)
229+
defer mgr.shutdown()
230+
231+
if _, err := mgr.open("tn1", 5555, "127.0.0.1"); err != nil {
232+
t.Fatalf("open: %v", err)
233+
}
234+
235+
writeDone := make(chan error, 1)
236+
go func() {
237+
writeDone <- mgr.writeData("tn1", "missing", []byte("payload"))
238+
}()
239+
240+
time.Sleep(50 * time.Millisecond)
241+
if ok := mgr.closeTunnel("tn1", "test close"); !ok {
242+
t.Fatal("closeTunnel returned false")
243+
}
244+
245+
select {
246+
case err := <-writeDone:
247+
if err == nil {
248+
t.Fatal("writeData unexpectedly succeeded")
249+
}
250+
case <-time.After(500 * time.Millisecond):
251+
t.Fatal("writeData did not unblock after tunnel close")
252+
}
253+
}
254+
159255
func TestTunnelManagerRejectsNonLoopback(t *testing.T) {
160256
mgr := newTunnelMgr(newFakeTunnelBridge())
161257
if _, err := mgr.open("tn1", 5555, "8.8.8.8"); err == nil {

0 commit comments

Comments
 (0)