carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "fmt"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8 "time"
9)
10
11// The dial/serve/handshake tests for this channel live in
12// internal/tunnelclient, where that code now lives. What is testable HERE is
13// the agent's wrapper: the env convention, and the state transitions the
14// status heartbeat reads off tunnelState().
15
16// TestTunnelLoopStateTransitions drives tunnelLoop against a fake edge that
17// hijacks, answers 101, then holds the raw conn open until the test releases
18// it — enough to observe tunnelState() go "" -> "connected", and then
19// "connected" -> "reconnecting" once the conn is torn down. This is the state
20// Task 6's heartbeat probe reads, so its transitions are worth asserting
21// through the wrapper that publishes them, not only in the package that
22// reports them.
23func TestTunnelLoopStateTransitions(t *testing.T) {
24 release := make(chan struct{})
25 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26 conn, bufrw, err := w.(http.Hijacker).Hijack()
27 if err != nil {
28 t.Errorf("hijack: %v", err)
29 return
30 }
31 fmt.Fprintf(bufrw, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: carlos-tunnel/1\r\nConnection: Upgrade\r\n\r\n")
32 bufrw.Flush()
33 <-release
34 conn.Close()
35 }))
36 defer srv.Close()
37
38 t.Setenv("CARLOS_TUNNEL_URL", srv.URL)
39 t.Setenv("CARLOS_TUNNEL_TOKEN", "sekrit")
40 t.Setenv("CARLOS_TUNNEL_NAME", "pi-box")
41
42 // tunnelLoop never returns while configured (by design — it retries
43 // forever on context.Background()), so this goroutine outlives the test;
44 // the fake edge is torn down via defer, and any further redial attempts
45 // just fail fast against a closed listener.
46 go tunnelLoop(http.NewServeMux())
47
48 waitForTunnelState(t, "connected")
49
50 close(release)
51
52 waitForTunnelState(t, "reconnecting")
53}
54
55// TestTunnelLoopInertUnlessFullyConfigured checks the documented convention:
56// tunnelLoop must return immediately — never dial, never block — unless all
57// three of CARLOS_TUNNEL_URL/_TOKEN/_NAME are set.
58func TestTunnelLoopInertUnlessFullyConfigured(t *testing.T) {
59 cases := []struct {
60 name, url, token, tname string
61 }{
62 {"all unset", "", "", ""},
63 {"url only", "http://tunnel.invalid", "", ""},
64 {"token only", "", "tok", ""},
65 {"name only", "", "", "pi-box"},
66 {"url and token", "http://tunnel.invalid", "tok", ""},
67 {"url and name", "http://tunnel.invalid", "", "pi-box"},
68 {"token and name", "", "tok", "pi-box"},
69 }
70 for _, c := range cases {
71 t.Run(c.name, func(t *testing.T) {
72 t.Setenv("CARLOS_TUNNEL_URL", c.url)
73 t.Setenv("CARLOS_TUNNEL_TOKEN", c.token)
74 t.Setenv("CARLOS_TUNNEL_NAME", c.tname)
75
76 done := make(chan struct{})
77 go func() {
78 tunnelLoop(http.NewServeMux())
79 close(done)
80 }()
81 select {
82 case <-done:
83 case <-time.After(500 * time.Millisecond):
84 t.Fatal("tunnelLoop did not return promptly with an incomplete env config")
85 }
86 })
87 }
88}
89
90func waitForTunnelState(t *testing.T, want string) {
91 t.Helper()
92 deadline := time.Now().Add(2 * time.Second)
93 for time.Now().Before(deadline) {
94 if tunnelState() == want {
95 return
96 }
97 time.Sleep(5 * time.Millisecond)
98 }
99 t.Fatalf("tunnelState() = %q, want %q before deadline", tunnelState(), want)
100}
101