package edge // Task 4: edge fleet-route dispatch — the 1-2-3 binding/fallback/dark rule // (task-4-brief.md): // // 1. Route.FleetBox != "" and channel "/" connected -> that // channel. // 2. else any connected channel of the fleet -> that one (deterministic: // ConnectedOfFleet's sorted order, first entry). // 3. else 503 with Retry-After: 2 (the fleet is dark). // // The bound/fallback cases need REAL h2 channels (a fake conn can't answer a // distinguishable response), so this uses the same tunnelclient.Loop-backed // fake remote host as tunnel_e2e_test.go rather than net.Pipe stand-ins. import ( "net" "net/http" "net/http/httptest" "net/url" "testing" "github.com/carlosframework/platform/internal/registry" ) // namedHandler answers every request with name, so a test can tell which // fake remote host actually served a proxied request. func namedHandler(name string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(name)) }) } // fleetTestEdge builds an Edge + loopback server + armed hub over a registry // carrying fleet "acme" with the given boxes (label -> token) and the one // route, then wires fleet channel auth into the hub (Reload, same as // production's Watch loop). Returns the server URL and the hub so a test can // start fake remote hosts and wait on connection state. func fleetTestEdge(t *testing.T, boxes map[string]string, rt registry.Route) (string, *TunnelHub) { t.Helper() reg := openReg(t) if err := reg.UpsertFleet(registry.Fleet{Name: "acme", Account: "amadan", Bucket: "acme-data"}); err != nil { t.Fatalf("UpsertFleet: %v", err) } for label, token := range boxes { if err := reg.AddFleetBox(registry.FleetBox{Fleet: "acme", Label: label, TokenHash: sha256Hex(token), Status: "active"}); err != nil { t.Fatalf("AddFleetBox(%s): %v", label, err) } } if err := reg.Upsert(rt); err != nil { t.Fatalf("upsert route: %v", err) } e, err := New(reg, nil) if err != nil { t.Fatalf("New: %v", err) } srv := httptest.NewServer(e) t.Cleanup(srv.Close) u, err := url.Parse(srv.URL) if err != nil { t.Fatal(err) } hub := NewTunnelHub(u.Hostname(), nil) e.EnableTunnels(hub) if err := e.Reload(); err != nil { t.Fatalf("Reload (wire fleet auth): %v", err) } return srv.URL, hub } // getHostFull is getHost (edge_test.go) but returns the whole response, so a // test can also inspect headers (Retry-After). func getHostFull(t *testing.T, edgeURL, host string) *http.Response { t.Helper() req, err := http.NewRequest("GET", edgeURL+"/", nil) if err != nil { t.Fatalf("new request: %v", err) } req.Host = host resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("GET %s (host %s): %v", edgeURL, host, err) } return resp } // TestFleetRouteProxiesToBoundBox: rule 1 — a route bound (FleetBox) to a // box whose channel is connected rides that exact channel. Bound to pi-2 // DELIBERATELY, not pi-1: ConnectedOfFleet sorts ascending, so with both // connected the fallback rule (rule 2) would ALSO pick "acme/pi-1" — binding // there would make rule 1 and rule 2 indistinguishable (both answer "pi-1"), // and this test would stay green even with the entire rule-1 branch deleted // from fleetChannelFor. Binding to pi-2 (which sorts LAST) means only rule 1 // can produce "pi-2" here — the fallback alone would answer "pi-1". func TestFleetRouteProxiesToBoundBox(t *testing.T) { edgeURL, hub := fleetTestEdge(t, map[string]string{"pi-1": "tok1", "pi-2": "tok2"}, registry.Route{ Host: "app.example", Kind: "instance", Status: "ready", Region: registry.PlacementTarget("acme"), FleetBox: "pi-2", }) stop1 := startFakeRemoteHost(t, edgeURL, "acme/pi-1", "tok1", namedHandler("pi-1")) defer stop1() stop2 := startFakeRemoteHost(t, edgeURL, "acme/pi-2", "tok2", namedHandler("pi-2")) defer stop2() waitForTunnelState(t, hub, "acme/pi-1", "connected", e2eReconnectBound) waitForTunnelState(t, hub, "acme/pi-2", "connected", e2eReconnectBound) code, body := getHost(t, edgeURL, "app.example") if code != http.StatusOK || body != "pi-2" { t.Fatalf("got %d %q, want 200 %q (the bound box, which sorts LAST — a fallback-only implementation would answer pi-1 instead)", code, body, "pi-2") } } // TestFleetRouteFallsOverWhenBoundBoxIsDark: rule 2 — the bound box (pi-1) // never connects at all; the only connected channel of the fleet (pi-2) // serves the request instead of a 502/503. func TestFleetRouteFallsOverWhenBoundBoxIsDark(t *testing.T) { edgeURL, hub := fleetTestEdge(t, map[string]string{"pi-1": "tok1", "pi-2": "tok2"}, registry.Route{ Host: "app.example", Kind: "instance", Status: "ready", Region: registry.PlacementTarget("acme"), FleetBox: "pi-1", }) // pi-1 is never dialed — bound but dark. stop2 := startFakeRemoteHost(t, edgeURL, "acme/pi-2", "tok2", namedHandler("pi-2")) defer stop2() waitForTunnelState(t, hub, "acme/pi-2", "connected", e2eReconnectBound) if hub.State("acme/pi-1") == "connected" { t.Fatal("test setup: pi-1 must not be connected for this case") } code, body := getHost(t, edgeURL, "app.example") if code != http.StatusOK || body != "pi-2" { t.Fatalf("got %d %q, want 200 %q (fallover to the only connected box)", code, body, "pi-2") } } // TestFleetRouteDarkFleetAnswers503: rule 3 — no channel of the fleet is // connected at all (bound or otherwise): a clean 503 + Retry-After: 2, not // the tunnel path's 502. func TestFleetRouteDarkFleetAnswers503(t *testing.T) { edgeURL, _ := fleetTestEdge(t, map[string]string{"pi-1": "tok1"}, registry.Route{ Host: "app.example", Kind: "instance", Status: "ready", Region: registry.PlacementTarget("acme"), FleetBox: "pi-1", }) // Nothing ever dials in. resp := getHostFull(t, edgeURL, "app.example") defer resp.Body.Close() if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("got %d, want 503", resp.StatusCode) } if got := resp.Header.Get("Retry-After"); got != "2" { t.Fatalf("Retry-After = %q, want %q", got, "2") } } // TestFleetRouteWithNoBoundBoxUsesFallback: FleetBox can legitimately be "" // (placement wants a fleet but no box has been assigned yet) — that must // skip straight to the fallback rule rather than erroring on an empty bound // channel name. func TestFleetRouteWithNoBoundBoxUsesFallback(t *testing.T) { edgeURL, hub := fleetTestEdge(t, map[string]string{"pi-1": "tok1"}, registry.Route{ Host: "app.example", Kind: "instance", Status: "ready", Region: registry.PlacementTarget("acme"), FleetBox: "", }) stop1 := startFakeRemoteHost(t, edgeURL, "acme/pi-1", "tok1", namedHandler("pi-1")) defer stop1() waitForTunnelState(t, hub, "acme/pi-1", "connected", e2eReconnectBound) code, body := getHost(t, edgeURL, "app.example") if code != http.StatusOK || body != "pi-1" { t.Fatalf("got %d %q, want 200 %q", code, body, "pi-1") } } // TestExplicitTunnelPrecedesFleetPlacement: a route can carry both an // explicit Tunnel binding and a fleet Region ("carlos route -tunnel" doesn't // clear Region, mirroring the existing Tunnel-vs-Socket/Addr precedence) — // the explicit Tunnel wins, per the brief's "Explicit rt.Tunnel keeps // precedence (legacy/tito path unchanged)". func TestExplicitTunnelPrecedesFleetPlacement(t *testing.T) { reg := openReg(t) if err := reg.UpsertFleet(registry.Fleet{Name: "acme", Account: "amadan", Bucket: "acme-data"}); err != nil { t.Fatal(err) } if err := reg.AddFleetBox(registry.FleetBox{Fleet: "acme", Label: "pi-1", TokenHash: sha256Hex("tok1"), Status: "active"}); err != nil { t.Fatal(err) } if err := reg.Upsert(registry.Route{ Host: "app.example", Kind: "instance", Status: "ready", Region: registry.PlacementTarget("acme"), FleetBox: "pi-1", Tunnel: "legacy-box", }); err != nil { t.Fatal(err) } e, err := New(reg, nil) if err != nil { t.Fatal(err) } srv := httptest.NewServer(e) defer srv.Close() u, err := url.Parse(srv.URL) if err != nil { t.Fatal(err) } hub := NewTunnelHub(u.Hostname(), map[string]string{"legacy-box": "sekrit"}) e.EnableTunnels(hub) if err := e.Reload(); err != nil { t.Fatal(err) } stop := startFakeRemoteHost(t, srv.URL, "legacy-box", "sekrit", namedHandler("legacy")) defer stop() waitForTunnelState(t, hub, "legacy-box", "connected", e2eReconnectBound) // The fleet channel (acme/pi-1) is never dialed at all — if the fleet // path were somehow taken, this would 503, not serve "legacy". code, body := getHost(t, srv.URL, "app.example") if code != http.StatusOK || body != "legacy" { t.Fatalf("got %d %q, want 200 %q (explicit Tunnel must win over fleet placement)", code, body, "legacy") } } // TestNonFleetRoutesUntouchedByFleetDispatch: an ordinary Addr-backed route // with no fleet placement at all (Region == "") must be completely unaffected // by the new dispatch case — same behavior as before Task 4. func TestNonFleetRoutesUntouchedByFleetDispatch(t *testing.T) { backend, _ := hostEchoBackend(t, "plain backend") reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "plain.example", Kind: "service", Status: "ready", Addr: backend.Listener.Addr().String(), }); err != nil { t.Fatal(err) } e, err := New(reg, nil) if err != nil { t.Fatal(err) } srv := httptest.NewServer(e) defer srv.Close() code, body := getHost(t, srv.URL, "plain.example") if code != http.StatusOK || body != "plain backend" { t.Fatalf("got %d %q, want 200 %q", code, body, "plain backend") } } // TestFleetRouteIsNeverActivatorManaged mirrors // TestTunnelRouteIsNeverActivatorManaged (tunnel_test.go) for the fleet // dispatch case: a fleet-placed, Hibernate:true route must never be // activator-managed on THIS box — whichever box ends up serving it wakes its // OWN local instance, so a platform-side wake here would start a second copy // beside it. Today this rests on viaTunnel being set for the fleet case too // (edge.go); this test is the regression pin so a future reorder or refactor // of that switch can't silently drop it — the exclusion would otherwise only // be caught by the tunnel-specific test above, which a fleet-only regression // wouldn't touch at all. func TestFleetRouteIsNeverActivatorManaged(t *testing.T) { reg := openReg(t) if err := reg.Upsert(registry.Route{ Host: "fleet-hib.example", Kind: "instance", Status: "ready", Region: registry.PlacementTarget("acme"), FleetBox: "pi-1", Hibernate: true, }); err != nil { t.Fatal(err) } act := &fakeActivator{} e, err := New(reg, act) if err != nil { t.Fatal(err) } e.EnableTunnels(NewTunnelHub("tunnel.test", nil)) if err := e.Reload(); err != nil { t.Fatal(err) } if last := act.retired[len(act.retired)-1]; last["fleet-hib.example"] { t.Fatalf("fleet-served host kept in the activator's managed set: %v", last) } req := httptest.NewRequest("GET", "http://fleet-hib.example/", nil) req.Host = "fleet-hib.example" e.ServeHTTP(httptest.NewRecorder(), req) if act.served != 0 { t.Fatalf("activator woke a local process for a fleet-served host (%d call(s))", act.served) } } // TestConnectedOfFleetSortedAndFiltered pins ConnectedOfFleet directly: only // names under "/" come back, sorted, and a same-prefixed different // fleet ("acme2") is never mistaken for a member of "acme". func TestConnectedOfFleetSortedAndFiltered(t *testing.T) { hub := NewTunnelHub("tunnel.test", nil) for _, name := range []string{"acme/pi-2", "acme/pi-1", "acme2/pi-1", "other/pi-1"} { c1, c2 := net.Pipe() t.Cleanup(func() { c2.Close() }) hub.mu.Lock() hub.conns[name] = &tunnelConn{raw: c1} hub.mu.Unlock() } got := hub.ConnectedOfFleet("acme") want := []string{"acme/pi-1", "acme/pi-2"} if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] { t.Fatalf("ConnectedOfFleet(acme) = %v, want %v", got, want) } }