package web
import (
"bytes"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/health"
"craftdesign.group/quarry/internal/ops"
)
// fakeSessions resolves one cookie to one user and answers Allow from a
// role table keyed by repo. The empty repo is the site-admin question.
type fakeSessions struct {
user string
roles map[string]ops.Auth
}
func (f fakeSessions) Login(string, string) (string, time.Time, bool) { return "", time.Time{}, false }
func (f fakeSessions) Resolve(tok string) (string, bool) { return f.user, tok == "good" }
func (f fakeSessions) Logout(string) {}
func (f fakeSessions) Allow(user, repo string, need ops.Auth) bool {
if user != f.user {
return false
}
return f.roles[repo] >= need
}
func withCookie(path string) *http.Request {
req := httptest.NewRequest("GET", path, nil)
req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
return req
}
// The fleet pages are gated at Admin with an empty repo. A user with
// write — even ADMIN — on a repository is not thereby a site admin, and
// the fleet surface exposes every store's operational detail. Getting
// this wrong turns "can edit one wiki" into "can read the whole fleet's
// error strings and force syncs across every store".
func TestFleetGateRefusesNonSiteAdmins(t *testing.T) {
verify := Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied })
for _, tc := range []struct {
name string
roles map[string]ops.Auth
want bool
}{
{"write on one repo", map[string]ops.Auth{"alpha": ops.Token}, false},
{"admin on one repo", map[string]ops.Auth{"alpha": ops.Admin}, false},
{"no roles at all", map[string]ops.Auth{}, false},
{"site admin", map[string]ops.Auth{"": ops.Admin}, true},
} {
t.Run(tc.name, func(t *testing.T) {
sess := fakeSessions{user: "someone", roles: tc.roles}
w := httptest.NewRecorder()
_, ok := gateAt(verify, sess, "", ops.Admin, w, withCookie("/fleet"))
if ok != tc.want {
t.Fatalf("gateAt admitted=%v, want %v (status %d)", ok, tc.want, w.Code)
}
if !tc.want && w.Code != http.StatusForbidden {
t.Fatalf("refusal status = %d, want 403", w.Code)
}
})
}
}
// A site admin must still be refused the per-repo write gate on a repo
// they hold no role for — gateAt must not have widened gate's meaning.
// (Site admins DO get every repo in the real role table; this asserts
// the level is what is consulted, not that admins are special-cased in
// the web layer.)
func TestGateStillAsksForTokenNotAdmin(t *testing.T) {
verify := Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied })
sess := fakeSessions{user: "someone", roles: map[string]ops.Auth{"alpha": ops.Token}}
if _, ok := gate(verify, sess, "alpha", httptest.NewRecorder(), withCookie("/r/alpha/wiki/x/edit")); !ok {
t.Fatal("token-level user refused a write page")
}
w := httptest.NewRecorder()
if _, ok := gate(verify, sess, "beta", w, withCookie("/r/beta/wiki/x/edit")); ok {
t.Fatal("user with no role on beta admitted to beta's write page")
}
if w.Code != http.StatusForbidden {
t.Fatalf("refusal status = %d, want 403", w.Code)
}
}
// The refusal message must not name a repository when the gate was not
// scoped to one: "someone has no write access to ”" is the shape that
// sends an operator hunting for a repo with an empty name.
func TestForbidWordsMatchTheGate(t *testing.T) {
for _, tc := range []struct {
repo string
level ops.Auth
want string
}{
{"", ops.Admin, "someone is not a site admin"},
{"alpha", ops.Admin, "someone has no admin access to alpha"},
{"alpha", ops.Token, "someone has no write access to alpha"},
} {
w := httptest.NewRecorder()
forbid(w, "someone", tc.repo, tc.level)
if got := w.Body.String(); got != tc.want {
t.Errorf("forbid(%q, %v) = %q, want %q", tc.repo, tc.level, got, tc.want)
}
}
}
// The dependency banner RENDERS. It shipped once calling a template
// helper with the wrong type — an execution-time failure, so it built
// clean, passed every test that did not render it, and served a bare
// "template error" to the one page that reports outages. A panel that
// breaks when something is down is worse than no panel.
func TestFleetBannerRendersWhenADependencyIsDown(t *testing.T) {
down := health.State{
Dep: health.Dep{Name: "notify", URL: "http://127.0.0.1:3000", Why: "no alerts are being delivered"},
Up: false,
Since: time.Now().Add(-3 * time.Hour),
Err: "connection refused",
}
var buf bytes.Buffer
err := noHooks.template(fleetAdminPage).Execute(&buf, pageData{Title: "Fleet", HideH1: true,
FleetTab: "stores", DepsDown: []health.State{down}})
if err != nil {
t.Fatalf("fleet page does not render with a dependency down: %v", err)
}
html := buf.String()
for _, want := range []string{"notify is down", "no alerts are being delivered",
"connection refused", "3h0m"} {
if !strings.Contains(html, want) {
t.Errorf("banner does not say %q", want)
}
}
}
// Silence when everything is fine: a banner listing healthy services is
// a banner people learn to ignore.
func TestFleetBannerIsAbsentWhenHealthy(t *testing.T) {
var buf bytes.Buffer
if err := noHooks.template(fleetAdminPage).Execute(&buf, pageData{Title: "Fleet", HideH1: true, FleetTab: "stores"}); err != nil {
t.Fatal(err)
}
if strings.Contains(buf.String(), "dep-down") {
t.Error("a healthy forge is showing an outage banner")
}
}