Files
felhom.eu/hub/internal/web/mailhold_test.go
T

136 lines
4.6 KiB
Go

package web
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/mailhold"
)
// MAIL-HOLD on the operator UI: the banner on every operator page while the marker exists, and the release endpoint
// behind the operator's auth + the R-135 CSRF gate.
const mailHoldBannerText = "E-mail is on hold"
func holdIn(t *testing.T, held bool) (*mailhold.Hold, string) {
t.Helper()
dir := t.TempDir()
marker := filepath.Join(dir, mailhold.FileName)
if held {
if err := os.WriteFile(marker, nil, 0o644); err != nil {
t.Fatal(err)
}
}
return mailhold.New(dir, nil), marker
}
func getPage(t *testing.T, s *Server, path string) string {
t.Helper()
r := httptest.NewRequest(http.MethodGet, path, nil)
w := httptest.NewRecorder()
s.ServeHTTP(w, r)
if w.Code != http.StatusOK {
t.Fatalf("GET %s: %d", path, w.Code)
}
return w.Body.String()
}
// Both branches of the {{if mailHeld}} gate, on the dashboard and on Configuration (which also carries the button).
func TestMailHoldBanner_RendersOnlyWhileHeld(t *testing.T) {
for _, held := range []bool{true, false} {
s, _ := newTestServer(t)
h, _ := holdIn(t, held)
s.SetMailHold(h)
for _, p := range []string{"/", "/configuration", "/hosts", "/configs"} {
body := getPage(t, s, p)
if got := strings.Contains(body, mailHoldBannerText); got != held {
t.Errorf("held=%v GET %s: banner shown=%v", held, p, got)
}
}
cfg := getPage(t, s, "/configuration")
if got := strings.Contains(cfg, `action="/configuration/mail-hold/release"`); got != held {
t.Errorf("held=%v: release button shown=%v", held, got)
}
}
}
// No hold wired at all (nil): no banner, pages render.
func TestMailHoldBanner_NilHoldRendersNoBanner(t *testing.T) {
s, _ := newTestServer(t)
if strings.Contains(getPage(t, s, "/"), mailHoldBannerText) {
t.Fatal("a nil hold must render no banner")
}
}
// The operator's release, through the production wiring (RequireAuth around ServeHTTP) with a session and its CSRF
// token: the marker is gone, and the banner with it.
func TestMailHoldRelease_RemovesMarker(t *testing.T) {
s, h := r135Handler(t)
hold, marker := holdIn(t, true)
s.SetMailHold(hold)
s.sessionsMu.Lock()
s.sessions["sess1"] = &hubSession{expiresAt: time.Now().Add(time.Hour), csrfToken: "tok1"}
s.sessionsMu.Unlock()
w := r135Post(h, "/configuration/mail-hold/release", func(r *http.Request) {
r.AddCookie(&http.Cookie{Name: SessionCookieName, Value: "sess1"})
r.Header.Set("X-CSRF-Token", "tok1")
})
if w.Code != http.StatusSeeOther || !strings.Contains(w.Header().Get("Location"), "flash=mail_hold_released") {
t.Fatalf("release: %d Location=%q", w.Code, w.Header().Get("Location"))
}
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("the marker is still there after the release: %v", err)
}
if hold.Held() {
t.Fatal("still held after the release")
}
}
// Refusals: Basic auth without the operator header (the R-135 cross-site shape), a session without its token, and no
// credentials at all. The marker stays in every case.
func TestMailHoldRelease_RefusedWithoutOperatorAuthOrCSRF(t *testing.T) {
s, h := r135Handler(t)
hold, marker := holdIn(t, true)
s.SetMailHold(hold)
s.sessionsMu.Lock()
s.sessions["sess1"] = &hubSession{expiresAt: time.Now().Add(time.Hour), csrfToken: "tok1"}
s.sessionsMu.Unlock()
cases := []struct {
name string
mut func(*http.Request)
ok func(int) bool
}{
{"basic without operator header", func(r *http.Request) { r.SetBasicAuth("", "op-pass") }, func(c int) bool { return c == http.StatusForbidden }},
{"session without token", func(r *http.Request) { r.AddCookie(&http.Cookie{Name: SessionCookieName, Value: "sess1"}) }, func(c int) bool { return c == http.StatusForbidden }},
{"no credentials", func(r *http.Request) { r.Header.Set(OperatorCLIHeader, "cli") }, func(c int) bool { return c == http.StatusFound || c == http.StatusUnauthorized }},
}
for _, c := range cases {
w := r135Post(h, "/configuration/mail-hold/release", c.mut)
if !c.ok(w.Code) {
t.Errorf("%s: status %d", c.name, w.Code)
}
if _, err := os.Stat(marker); err != nil {
t.Fatalf("%s: the marker was removed by a refused request: %v", c.name, err)
}
}
// The CLI shape (Basic + the operator header) passes.
w := r135Post(h, "/configuration/mail-hold/release", func(r *http.Request) {
r.SetBasicAuth("", "op-pass")
r.Header.Set(OperatorCLIHeader, "cli")
})
if w.Code != http.StatusSeeOther {
t.Fatalf("operator CLI release: %d", w.Code)
}
if hold.Held() {
t.Fatal("operator CLI release left the hold in place")
}
}