| new file mode 100644 |
| index 000000000..0951bc54f |
| --- /dev/null |
| +++ b/internal/edge/alias_test.go |
| @@ -0,0 +1,277 @@ |
| +package edge |
| + |
| +import ( |
| + "net/http" |
| + "net/http/httptest" |
| + "slices" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +// upsertAlias writes one route_aliases row, the shape the reconcile pass |
| +// (internal/domains) derives from a claim record. The edge only ever reads |
| +// Alias and Host; account/app are carried for realism. |
| +func upsertAlias(t *testing.T, reg *registry.Registry, alias, host string) { |
| + t.Helper() |
| + if err := reg.UpsertAlias(registry.RouteAlias{ |
| + Alias: alias, Host: host, Account: "acct", App: "app", CreatedAt: 1, |
| + }); err != nil { |
| + t.Fatalf("upsert alias %s -> %s: %v", alias, host, err) |
| + } |
| +} |
| + |
| +// TestAliasServesPrimaryBackend is the feature itself: a hostname attached to |
| +// a ready route reaches that route's backend, with the customer's own Host |
| +// header preserved end to end (the app must see the name the browser asked |
| +// for, not the platform hostname). |
| +func TestAliasServesPrimaryBackend(t *testing.T) { |
| + backend, seenHost := hostEchoBackend(t, "hello from the primary") |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "app.acct.oncarlos.com", Kind: "service", |
| + Addr: backend.Listener.Addr().String(), Status: "ready", |
| + }); err != nil { |
| + t.Fatalf("upsert: %v", err) |
| + } |
| + upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com") |
| + |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatalf("New: %v", err) |
| + } |
| + srv := httptest.NewServer(e) |
| + defer srv.Close() |
| + |
| + code, body := getHost(t, srv.URL, "www.customer.example") |
| + if code != http.StatusOK { |
| + t.Fatalf("status = %d, want 200 for an attached hostname", code) |
| + } |
| + if body != "hello from the primary" { |
| + t.Fatalf("body = %q, want the primary route's backend body", body) |
| + } |
| + if *seenHost != "www.customer.example" { |
| + t.Fatalf("backend saw Host %q, want www.customer.example (inbound Host must be preserved)", *seenHost) |
| + } |
| +} |
| + |
| +// TestAliasSharesUpstreamEntry is the composition pin (#152, multi-upstream |
| +// HA): the alias must map to the primary's OWN entry — the identical proxy |
| +// handler value and the primary's route row — never a proxy rebuilt for the |
| +// alias hostname. Everything downstream is keyed off route.Host (activator |
| +// entry, lease, live symlink, cgroup) and off the proxy value's per-primary |
| +// state, so a second handler would silently fork both. |
| +func TestAliasSharesUpstreamEntry(t *testing.T) { |
| + backend, _ := hostEchoBackend(t, "primary") |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "app.acct.oncarlos.com", Kind: "service", |
| + Addr: backend.Listener.Addr().String(), Status: "ready", |
| + }); err != nil { |
| + t.Fatalf("upsert: %v", err) |
| + } |
| + upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com") |
| + |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatalf("New: %v", err) |
| + } |
| + m := e.current() |
| + primary, ok := m["app.acct.oncarlos.com"] |
| + if !ok { |
| + t.Fatalf("primary route missing from the upstream table") |
| + } |
| + alias, ok := m["www.customer.example"] |
| + if !ok { |
| + t.Fatalf("alias missing from the upstream table") |
| + } |
| + // Identity, not equivalent behaviour: the route carries no Version, so the |
| + // handler is the *httputil.ReverseProxy pointer itself and == compares the |
| + // pointer, which is exactly the claim being pinned. |
| + if alias.proxy != primary.proxy { |
| + t.Fatalf("alias proxy %p is not the primary's proxy %p — an alias must never get a rebuilt handler", alias.proxy, primary.proxy) |
| + } |
| + if alias.route.Host != "app.acct.oncarlos.com" { |
| + t.Fatalf("alias route.Host = %q, want the PRIMARY's host (activator/version/purge keying all read it)", alias.route.Host) |
| + } |
| + if !alias.alias { |
| + t.Fatalf("alias entry not marked alias — wildcard parent derivation would treat a customer domain as a platform host") |
| + } |
| + if primary.alias { |
| + t.Fatalf("primary entry marked alias") |
| + } |
| +} |
| + |
| +// TestAliasAllowed covers the autocert HostPolicy: an attached hostname is a |
| +// host this box serves, so it may be certed; a stranger's SNI may not. |
| +func TestAliasAllowed(t *testing.T) { |
| + backend, _ := hostEchoBackend(t, "primary") |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "app.acct.oncarlos.com", Kind: "service", |
| + Addr: backend.Listener.Addr().String(), Status: "ready", |
| + }); err != nil { |
| + t.Fatalf("upsert: %v", err) |
| + } |
| + upsertAlias(t, reg, "www.customer.example", "app.acct.oncarlos.com") |
| + |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatalf("New: %v", err) |
| + } |
| + if !e.Allowed("www.customer.example") { |
| + t.Fatalf("Allowed(www.customer.example) = false, want true for an attached hostname") |
| + } |
| + if e.Allowed("stranger.example") { |
| + t.Fatalf("Allowed(stranger.example) = true, want false") |
| + } |
| +} |
| + |
| +// TestAliasNeverShadowsRoute: routes always win. An alias row whose hostname |
| +// collides with a real route must not displace it — the route's own backend |
| +// keeps answering. |
| +func TestAliasNeverShadowsRoute(t *testing.T) { |
| + one, _ := hostEchoBackend(t, "one") |
| + two, _ := hostEchoBackend(t, "two") |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "one.example.com", Kind: "service", |
| + Addr: one.Listener.Addr().String(), Status: "ready", |
| + }); err != nil { |
| + t.Fatalf("upsert one: %v", err) |
| + } |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "two.example.com", Kind: "service", |
| + Addr: two.Listener.Addr().String(), Status: "ready", |
| + }); err != nil { |
| + t.Fatalf("upsert two: %v", err) |
| + } |
| + // An alias claiming a hostname that is already a route of its own. |
| + upsertAlias(t, reg, "two.example.com", "one.example.com") |
| + |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatalf("New: %v", err) |
| + } |
| + srv := httptest.NewServer(e) |
| + defer srv.Close() |
| + |
| + code, body := getHost(t, srv.URL, "two.example.com") |
| + if code != http.StatusOK || body != "two" { |
| + t.Fatalf("got %d %q, want 200 \"two\" — a real route must win a collision with an alias", code, body) |
| + } |
| + if up := e.current()["two.example.com"]; up.alias { |
| + t.Fatalf("route entry for two.example.com was overwritten by the alias") |
| + } |
| +} |
| + |
| +// TestAliasSkipsMissingPrimary: an alias whose primary is not in the table |
| +// (not ready here, or not local to this box) is skipped entirely rather than |
| +// mapped to nothing — it must 404 as an unknown host and must not be certed. |
| +func TestAliasSkipsMissingPrimary(t *testing.T) { |
| + backend, _ := hostEchoBackend(t, "should never be reached") |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "pending.acct.oncarlos.com", Kind: "service", |
| + Addr: backend.Listener.Addr().String(), Status: "pending", |
| + }); err != nil { |
| + t.Fatalf("upsert: %v", err) |
| + } |
| + upsertAlias(t, reg, "www.customer.example", "pending.acct.oncarlos.com") |
| + // And one pointing at a host with no route row at all. |
| + upsertAlias(t, reg, "gone.customer.example", "nosuch.acct.oncarlos.com") |
| + |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatalf("New: %v", err) |
| + } |
| + srv := httptest.NewServer(e) |
| + defer srv.Close() |
| + |
| + for _, host := range []string{"www.customer.example", "gone.customer.example"} { |
| + if code, _ := getHost(t, srv.URL, host); code != http.StatusNotFound { |
| + t.Fatalf("status for %s = %d, want 404 (alias with no ready primary)", host, code) |
| + } |
| + if e.Allowed(host) { |
| + t.Fatalf("Allowed(%s) = true, want false — no certificate for an alias to nowhere", host) |
| + } |
| + } |
| +} |
| + |
| +// routeRecordingActivator records the route each wake was handed, so a test |
| +// can prove which host the activator was keyed on. |
| +type routeRecordingActivator struct { |
| + routes []registry.Route |
| +} |
| + |
| +func (a *routeRecordingActivator) ServeHTTP(w http.ResponseWriter, r *http.Request, rt registry.Route, proxy http.Handler) bool { |
| + a.routes = append(a.routes, rt) |
| + proxy.ServeHTTP(w, r) // "wake" is instant; proxy straight through |
| + return true |
| +} |
| + |
| +func (a *routeRecordingActivator) RetireExcept(keep map[string]bool) {} |
| + |
| +// TestAliasWakesPrimaryOnce is the #129-class regression pin: a request to an |
| +// attached hostname wakes the PRIMARY's single activator entry. The activator |
| +// keys its entry, lease, live symlink and cgroup on route.Host, so an alias |
| +// arriving with its own host would start a second copy of the app beside the |
| +// first and fight it for the socket. |
| +func TestAliasWakesPrimaryOnce(t *testing.T) { |
| + backend, _ := hostEchoBackend(t, "woken") |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "hib.acct.oncarlos.com", Kind: "instance", |
| + Addr: backend.Listener.Addr().String(), Status: "ready", Hibernate: true, |
| + }); err != nil { |
| + t.Fatalf("upsert: %v", err) |
| + } |
| + upsertAlias(t, reg, "www.customer.example", "hib.acct.oncarlos.com") |
| + |
| + act := &routeRecordingActivator{} |
| + e, err := New(reg, act) |
| + if err != nil { |
| + t.Fatalf("New: %v", err) |
| + } |
| + srv := httptest.NewServer(e) |
| + defer srv.Close() |
| + |
| + code, body := getHost(t, srv.URL, "www.customer.example") |
| + if code != http.StatusOK || body != "woken" { |
| + t.Fatalf("got %d %q, want 200 woken", code, body) |
| + } |
| + if len(act.routes) != 1 { |
| + t.Fatalf("activator saw %d wakes, want exactly 1", len(act.routes)) |
| + } |
| + if got := act.routes[0].Host; got != "hib.acct.oncarlos.com" { |
| + t.Fatalf("activator was handed route.Host = %q, want the primary hib.acct.oncarlos.com", got) |
| + } |
| +} |
| + |
| +// TestCurrentHostsSkipsAliases: wildcard parent derivation reads currentHosts, |
| +// so a customer hostname riding a platform-minted primary must never appear |
| +// there — otherwise a name like x.y.<apps-domain> could mint a wildcard parent |
| +// and burn an ACME issuance on a domain the wildcard never covered. |
| +func TestCurrentHostsSkipsAliases(t *testing.T) { |
| + backend, _ := hostEchoBackend(t, "primary") |
| + reg := openReg(t) |
| + if err := reg.Upsert(registry.Route{ |
| + Host: "app.acct.oncarlos.com", Kind: "service", |
| + Addr: backend.Listener.Addr().String(), Status: "ready", Platform: true, |
| + }); err != nil { |
| + t.Fatalf("upsert: %v", err) |
| + } |
| + upsertAlias(t, reg, "shop.acct.oncarlos.com", "app.acct.oncarlos.com") |
| + |
| + e, err := New(reg, nil) |
| + if err != nil { |
| + t.Fatalf("New: %v", err) |
| + } |
| + got := e.currentHosts() |
| + if !slices.Contains(got, "app.acct.oncarlos.com") { |
| + t.Fatalf("currentHosts = %v, want it to contain the platform-minted primary", got) |
| + } |
| + if slices.Contains(got, "shop.acct.oncarlos.com") { |
| + t.Fatalf("currentHosts = %v, must NOT contain an attached hostname", got) |
| + } |
| +} |