fix(cloudcp): serve control plane favicon

This commit is contained in:
rcourtman 2026-03-26 10:48:42 +00:00
parent 140aa2bdb1
commit 8ecea8dbdf
5 changed files with 98 additions and 0 deletions

View file

@ -781,6 +781,7 @@ func TestPortalPageTemplate_AccountServicesRendered(t *testing.T) {
mustContain := []string{
"<title>Pulse Account</title>",
`<link rel="icon" href="/favicon.svg" type="image/svg+xml">`,
`id="portal-user-info"`,
`id="portal-app-root"`,
`id="pulse-account-bootstrap"`,
@ -1074,6 +1075,7 @@ func TestPortalPageTemplate_UsesPulseAccountBrandingWhenSignedOut(t *testing.T)
mustContain := []string{
"<title>Pulse Account</title>",
`<link rel="icon" href="/favicon.svg" type="image/svg+xml">`,
"Pulse Account",
`id="portal-app-root"`,
"Enter the commercial email address for your Pulse account.",

View file

@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pulse Account</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<style nonce="{{.Nonce}}">{{.Styles}}</style>
</head>
<body>

View file

@ -69,6 +69,8 @@ func RegisterRoutes(mux *http.ServeMux, deps *Deps) {
// Health / readiness are unauthenticated liveness/readiness probes.
mux.HandleFunc("/healthz", admin.HandleHealthz)
mux.HandleFunc("/readyz", admin.HandleReadyz(deps.Registry))
mux.HandleFunc("/favicon.svg", handleControlPlaneFaviconSVG)
mux.HandleFunc("/favicon.ico", handleControlPlaneFaviconICO)
// Status and metrics are private by default.
statusHandler := http.HandlerFunc(admin.HandleStatus(deps.Registry, deps.Version))

View file

@ -0,0 +1,60 @@
package cloudcp
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/registry"
)
func TestRegisterRoutes_ControlPlaneFavicons(t *testing.T) {
dir := t.TempDir()
reg, err := registry.NewTenantRegistry(dir)
if err != nil {
t.Fatalf("NewTenantRegistry: %v", err)
}
t.Cleanup(func() { _ = reg.Close() })
trialStore, err := NewTrialSignupStore(dir)
if err != nil {
t.Fatalf("NewTrialSignupStore: %v", err)
}
t.Cleanup(func() { trialStore.Close() })
mux := http.NewServeMux()
RegisterRoutes(mux, &Deps{
Config: &CPConfig{
DataDir: dir,
AdminKey: "test-admin-key",
BaseURL: "https://cloud.example.com",
StripeWebhookSecret: "whsec_test",
},
Registry: reg,
TrialSignupStore: trialStore,
Version: "test",
})
svgReq := httptest.NewRequest(http.MethodGet, "/favicon.svg", nil)
svgRec := httptest.NewRecorder()
mux.ServeHTTP(svgRec, svgReq)
if svgRec.Code != http.StatusOK {
t.Fatalf("GET /favicon.svg status=%d, want %d", svgRec.Code, http.StatusOK)
}
if got := svgRec.Header().Get("Content-Type"); got != "image/svg+xml" {
t.Fatalf("GET /favicon.svg content-type=%q, want %q", got, "image/svg+xml")
}
if !strings.Contains(svgRec.Body.String(), "<svg") {
t.Fatalf("expected svg favicon payload")
}
icoReq := httptest.NewRequest(http.MethodGet, "/favicon.ico", nil)
icoRec := httptest.NewRecorder()
mux.ServeHTTP(icoRec, icoReq)
if icoRec.Code != http.StatusMovedPermanently {
t.Fatalf("GET /favicon.ico status=%d, want %d", icoRec.Code, http.StatusMovedPermanently)
}
if got := icoRec.Header().Get("Location"); got != "/favicon.svg" {
t.Fatalf("GET /favicon.ico location=%q, want %q", got, "/favicon.svg")
}
}

View file

@ -0,0 +1,33 @@
package cloudcp
import "net/http"
const controlPlaneFaviconSVG = `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#0f172a"/>
<path d="M18 18h13c9.941 0 18 8.059 18 18s-8.059 18-18 18H18V18zm11 10h-3v16h3c4.418 0 8-3.582 8-8s-3.582-8-8-8z" fill="#38bdf8"/>
<circle cx="47" cy="18" r="5" fill="#1d4ed8"/>
</svg>
`
func handleControlPlaneFaviconSVG(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "image/svg+xml")
w.Header().Set("Cache-Control", "public, max-age=86400")
w.WriteHeader(http.StatusOK)
if r.Method == http.MethodHead {
return
}
_, _ = w.Write([]byte(controlPlaneFaviconSVG))
}
func handleControlPlaneFaviconICO(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
http.Redirect(w, r, "/favicon.svg", http.StatusMovedPermanently)
}