Files
base/apis/serve_test.go
zooqueen d3b35335c3 the admin is one UI with three places to be, and it fits all of them
Base renders standalone at its own host and as a section inside a Hanzo
surface. One bundle serves both: the same bytes the binary already embeds,
so a host is current the moment the pin moves, rather than a component
every host recompiles with its own copy of a transport that only works
against the Base that served it.

Who may frame the admin is a CSP source list, BASE_FRAME_ANCESTORS,
default 'self' — which is what the deployment did before. It is stated in
one place, on the admin, the only surface a frame can be aimed at to any
effect. X-Frame-Options goes rather than sitting beside it: it can say
"nobody" or "same origin" and cannot name a host, so an allow-list is the
one thing it cannot express, and a second weaker copy of a rule drifts
from the first. The ingress already says it this way in front of hanzo.id.

Auth is untouched. The framed admin runs its own PKCE, a live IAM session
returns a code without rendering anything, and no token crosses an origin.

The frame drops the brand and the account because the host answers both
already, read from window.self !== window.top — where a document is, is a
fact the browser holds, and a flag is a second answer that can be wrong.

The nav was a 14rem column at every width, which on a 390px viewport left
166px and put the table past the right edge, inside a pane that scrolls —
so the page never grew and no check written against the document could
see it. The strip is now the base layout and the column is what a wide
page of our own opts into, each declared once. smoke gates all three.
2026-08-13 18:09:43 -07:00

52 lines
1.8 KiB
Go

package apis
import (
"strings"
"testing"
)
// The admin renders standalone at its own host and inside the Hanzo surfaces
// that offer Base as a section, so who may frame it is deployment data. These
// pin the two halves of that: the default is this origin and nobody else, and
// what a deployment says is what the header carries.
func TestFrameAncestorsDefaultsToThisOriginAlone(t *testing.T) {
t.Setenv("BASE_FRAME_ANCESTORS", "")
if got := frameAncestors(); got != "'self'" {
t.Fatalf("expected the default to name this origin alone, got %q", got)
}
}
func TestFrameAncestorsCarriesTheHostsADeploymentNames(t *testing.T) {
const hosts = "'self' https://console.hanzo.ai https://hanzo.app"
t.Setenv("BASE_FRAME_ANCESTORS", hosts)
if got := frameAncestors(); got != hosts {
t.Fatalf("expected %q, got %q", hosts, got)
}
}
// Whitespace is what an env var picks up from YAML block scalars, and a value
// that is only whitespace is a deployment that said nothing.
func TestFrameAncestorsTreatsBlankAsUnset(t *testing.T) {
t.Setenv("BASE_FRAME_ANCESTORS", " ")
if got := frameAncestors(); got != "'self'" {
t.Fatalf("expected blank to read as unset, got %q", got)
}
}
// The directive has to reach the policy the admin is actually served with —
// a value read into a string nothing concatenates protects nothing. This is
// the same composition apis/serve.go performs on the admin response.
func TestAdminPolicyCarriesTheFrameRule(t *testing.T) {
t.Setenv("BASE_FRAME_ANCESTORS", "'self' https://console.hanzo.ai")
policy := "default-src 'self'; script-src 'self' 'unsafe-inline'; frame-ancestors " + frameAncestors()
if !strings.Contains(policy, "frame-ancestors 'self' https://console.hanzo.ai") {
t.Fatalf("frame rule missing from the served policy: %q", policy)
}
}