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)
}
}
}