package web import ( "io/fs" "regexp" "strings" "testing" ) func read(t *testing.T, name string) string { t.Helper() sub, err := fs.Sub(files, "static") if err != nil { t.Fatal(err) } b, err := fs.ReadFile(sub, name) if err != nil { t.Fatal(err) } return string(b) } // The sign-in button has to be a link the browser navigates, not script: the // CSP's connect-src is 'self', so a fetch to the identity provider is blocked, // and it is a redirect to the provider that the server answers. func TestLoginPageOffersSSOAsAPlainLink(t *testing.T) { html := read(t, "index.html") if !regexp.MustCompile(`]*id="sso-link"[^>]*href="/api/oidc/login"|]*href="/api/oidc/login"[^>]*id="sso-link"`).MatchString(html) { t.Error("index.html has no ") } if !strings.Contains(html, `id="password-login"`) { t.Error("the password fields must sit in #password-login so a server can hide them") } } // Every code the server can put in ?sso_error= must have a message, or a // refused person sees a generic failure and cannot tell what to ask for. func TestLoginExplainsEverySSOError(t *testing.T) { js := read(t, "js/app.js") for _, code := range []string{ "denied", "expired", "failed", "unavailable", "not_allowed", "no_email", "email_conflict", "disabled", } { if !regexp.MustCompile(`\b` + code + `:`).MatchString(js) { t.Errorf("app.js has no message for sso_error=%s", code) } } } // The page a terminal's prompt links to has to be reachable as a route, or the // link 404s into the queue and the code is never seen. func TestDevicePageIsRoutedAndCallsTheApprovalAPI(t *testing.T) { if !strings.Contains(read(t, "index.html"), `id="view-device"`) { t.Error("index.html has no #view-device section") } app := read(t, "js/app.js") if !strings.Contains(app, "name === 'device'") || !strings.Contains(app, "device: {") { t.Error("app.js does not route /device") } // The SSO button must carry the page asked for through the provider. if !strings.Contains(app, "/api/oidc/login?next=") { t.Error("the SSO link does not carry next=") } dev := read(t, "js/device.js") for _, want := range []string{"approveDevice", "denyDevice"} { if !strings.Contains(dev, want) { t.Errorf("device.js never calls %s", want) } } api := read(t, "js/api.js") for _, want := range []string{"/oidc/device/approve", "/oidc/device/deny"} { if !strings.Contains(api, want) { t.Errorf("api.js has no call to %s", want) } } } // SSO-managed access must be marked in every view that edits it. func TestManagedAccessIsMarkedWhereItIsEdited(t *testing.T) { for _, file := range []string{"js/team.js", "js/adminteam.js", "js/adminuser.js", "js/admin.js"} { js := read(t, file) if !strings.Contains(js, "ssoBadge") || !strings.Contains(js, "SSO_MANAGED") { t.Errorf("%s does not mark or explain SSO-managed access", file) } } }