Files
explorer/frontend_test.go
T
zeekay c37bb4e893 fix(frontend): match per-chain Brand for explore-{slug} / api-explore-{slug} hosts
brandStruct only matched bare "{slug}" and "{slug}.tld" host shapes —
but the unified explorer ships behind IngressRoutes whose Host()
patterns are exclusively `explore-{slug}.lux.network` and
`api-explore-{slug}.lux.network` plus brand-owned roots like
`explore.hanzo.ai`, `explore.zoo.ngo`, `explore.pars.network`. With
the old matcher every per-chain host fell through to BrandDefault
("Lux Explorer"), so per-chain theming silently no-op'd in prod
(verified 2026-06-05: explore-hanzo.lux.network /envs.js returned
"VITE_BRAND":{"name":"Lux Explorer"} instead of Hanzo).

New matcher recognises the four prefix shapes the IngressRoutes
produce, both with and without an env suffix (-test / -dev):
  - explore-{slug}.tld...        UI host on lux.network
  - api-explore-{slug}.tld...    API host on lux.network
  - explore-{slug}-{env}.tld...  UI host for testnet / devnet variants
  - api-explore-{slug}-{env}...  API host for testnet / devnet variants
  - explore.{slug}.tld...        UI on brand-owned domain
  - api-explore.{slug}.tld...    API on brand-owned domain
Plus the prior bare-{slug} and {slug}-prefix shapes for backward
compat.

Test coverage: TestBrandStructHostMatching, 18 cases incl. the
explore-spcfoo cross-contamination guard ensuring partial-slug-
suffix matches don't bleed.
2026-06-05 15:05:22 -07:00

182 lines
5.5 KiB
Go

package main
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
// TestFrontendStaticOverlay verifies that NewFrontend prefers the on-disk
// static dir over the embedded FS when cfg.StaticDir points at a real
// directory. The overlay use-case: ConfigMap-mounting a richer SPA shell
// into an existing image without rebuilding.
func TestFrontendStaticOverlay(t *testing.T) {
dir := t.TempDir()
want := "<!doctype html><title>OVERLAY</title>"
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(want), 0644); err != nil {
t.Fatal(err)
}
cfg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
f, err := NewFrontend(cfg, NewChainRegistry())
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
f.Mount(mux)
req := httptest.NewRequest("GET", "/", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
got := rec.Body.String()
if !strings.Contains(got, "OVERLAY") {
t.Fatalf("expected overlay index.html, got %q", got)
}
}
// TestFrontendNoOverlay verifies that with no StaticDir, the embedded
// index.html is served (current behaviour — pre-overlay).
func TestFrontendNoOverlay(t *testing.T) {
cfg := Config{BrandDefault: Brand{Name: "Test"}}
f, err := NewFrontend(cfg, NewChainRegistry())
if err != nil {
t.Fatal(err)
}
if f.overlay != nil {
t.Fatal("expected nil overlay when StaticDir empty")
}
mux := http.NewServeMux()
f.Mount(mux)
req := httptest.NewRequest("GET", "/", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
got := rec.Body.String()
if !strings.Contains(got, "doctype html") {
t.Fatalf("expected fallback index.html, got %q", got)
}
}
// TestFrontendOverlayMissingFileFallsBack covers the case where the
// overlay dir exists but a particular file is missing — the request
// falls through to the embedded FS (and ultimately to index.html for
// SPA-routing).
func TestFrontendOverlayMissingFileFallsBack(t *testing.T) {
dir := t.TempDir()
// Only index.html in overlay; no other assets.
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<!doctype html>OVERLAY"), 0644); err != nil {
t.Fatal(err)
}
cfg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
f, err := NewFrontend(cfg, NewChainRegistry())
if err != nil {
t.Fatal(err)
}
mux := http.NewServeMux()
f.Mount(mux)
// Request a path the overlay doesn't have. With SPA-routing, the
// handler returns index.html — which IS in the overlay.
req := httptest.NewRequest("GET", "/some/spa/route", nil)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
got := rec.Body.String()
if !strings.Contains(got, "OVERLAY") {
t.Fatalf("expected overlay index.html (SPA fallback), got %q", got)
}
}
// TestFrontendEnvStaticDir verifies $EXPLORER_STATIC_DIR is the env-var
// equivalent of cfg.StaticDir. Empty cfg + env-var-set overlay should
// still pick up the overlay.
func TestFrontendEnvStaticDir(t *testing.T) {
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("ENV-OVERLAY"), 0644); err != nil {
t.Fatal(err)
}
t.Setenv("EXPLORER_STATIC_DIR", dir)
cfg := Config{BrandDefault: Brand{Name: "Test"}}
f, err := NewFrontend(cfg, NewChainRegistry())
if err != nil {
t.Fatal(err)
}
if f.overlay == nil {
t.Fatal("expected overlay populated from env var")
}
if !strings.Contains(string(f.index), "ENV-OVERLAY") {
t.Fatalf("expected env-overlay index, got %q", string(f.index))
}
}
// TestBrandStructHostMatching covers every hostname shape the unified
// explorer's IngressRoutes hand it: the per-chain brand must win over
// BrandDefault for both UI hosts (explore-{slug}, explore.{slug}.tld)
// and API hosts (api-explore-{slug}). The matcher must NOT bleed across
// chains — a hanzo host must not pick zoo's brand or vice versa.
func TestBrandStructHostMatching(t *testing.T) {
hanzoBrand := Brand{Name: "Hanzo", Coin: "AI", AccentColor: "#0b1220"}
zooBrand := Brand{Name: "Zoo", Coin: "ZOO", AccentColor: "#7c3aed"}
cfg := Config{
BrandDefault: Brand{Name: "Lux Explorer"},
Chains: []ChainConfig{
{Slug: "cchain", Brand: &Brand{Name: "Lux C-Chain", Coin: "LUX"}},
{Slug: "hanzo", Brand: &hanzoBrand},
{Slug: "zoo", Brand: &zooBrand},
},
}
f, err := NewFrontend(cfg, NewChainRegistry())
if err != nil {
t.Fatal(err)
}
cases := []struct {
host string
want string
}{
// Bare slug + slug-prefix shape.
{"hanzo", "Hanzo"},
{"hanzo.lux.network", "Hanzo"},
// explore-{slug} prefix shape (lux.network UI hosts).
{"explore-hanzo.lux.network", "Hanzo"},
{"explore-hanzo-test.lux.network", "Hanzo"},
{"explore-hanzo-dev.lux.network", "Hanzo"},
{"explore-zoo.lux.network", "Zoo"},
// api-explore-{slug} prefix shape (lux.network API hosts).
{"api-explore-hanzo.lux.network", "Hanzo"},
{"api-explore-hanzo-test.lux.network", "Hanzo"},
{"api-explore-zoo.lux.network", "Zoo"},
// explore.{slug}.tld shape (brand-owned domains).
{"explore.hanzo.ai", "Hanzo"},
{"explore.hanzo.network", "Hanzo"},
{"explore.zoo.network", "Zoo"},
{"explore.zoo.ngo", "Zoo"},
{"api-explore.hanzo.ai", "Hanzo"},
{"api-explore.zoo.ngo", "Zoo"},
// Mismatched hosts fall back to BrandDefault.
{"explore.lux.network", "Lux Explorer"},
{"unknown.example.com", "Lux Explorer"},
// Cross-contamination guard: explore-spcfoo must NOT match spc.
{"explore-spcfoo.lux.network", "Lux Explorer"},
}
for _, tc := range cases {
t.Run(tc.host, func(t *testing.T) {
got := f.brandStruct(tc.host).Name
if got != tc.want {
t.Fatalf("host=%q: got brand=%q, want %q", tc.host, got, tc.want)
}
})
}
}