package web
import (
"bufio"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/ops"
)
// The operations panel is a stream, not a client timer. Every other
// live region on this forge holds one connection open; ops asked on
// data-on-interval, which charged a signed-in request, a snapshot and a
// full template render to every open tab every three seconds whether or
// not anything had moved.
func TestOpsPageStreamsRatherThanPolls(t *testing.T) {
var buf strings.Builder
if err := noHooks.template(opsPage).Execute(&buf, pageData{Title: "Operations",
Ops: opsView{Window: "no renders"}}); err != nil {
t.Fatal(err)
}
body := buf.String()
if strings.Contains(body, "data-on-interval") {
t.Error("the panel still polls on a client timer")
}
if !strings.Contains(body, `data-init="@get('/fleet/ops.sse')"`) {
t.Errorf("the panel does not open the stream:\n%s", body)
}
// The one-shot fragment it replaced must be gone, not merely
// unused: a route nothing opens is a route nobody maintains.
if strings.Contains(body, "ops.frag") {
t.Error("the panel still references the retired fragment route")
}
}
func opsStreamServer(t *testing.T) *httptest.Server {
t.Helper()
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "v",
Repos: api.NewRepos(map[string]*api.Repo{})}); err != nil {
t.Fatal(err)
}
srv := httptest.NewServer(HandlerWith(r, nil, nil, nil))
t.Cleanup(srv.Close)
return srv
}
// The stream opens as a stream: the reader gets the event-stream
// content type and the comment that says the connection is live before
// anything is asked of the registry, so a tab never waits on a snapshot
// to know it is connected.
func TestOpsStreamOpensLive(t *testing.T) {
srv := opsStreamServer(t)
req, err := http.NewRequest("GET", srv.URL+"/fleet/ops.sse", nil)
if err != nil {
t.Fatal(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatalf("status %d", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/event-stream") {
t.Fatalf("content-type %q", ct)
}
if cc := resp.Header.Get("Cache-Control"); cc != "no-store" {
t.Errorf("a live stream must not be cached, got %q", cc)
}
line, err := bufio.NewReader(resp.Body).ReadString('\n')
if err != nil {
t.Fatal(err)
}
if strings.TrimSpace(line) != ": live" {
t.Fatalf("stream did not announce itself: %q", line)
}
}
// The retired poll route answers nothing. Left registered it would go
// on serving a body no page reads, and the next reader of this file
// would have two paths to keep in step.
func TestOpsFragmentRouteIsGone(t *testing.T) {
srv := opsStreamServer(t)
resp, err := http.Get(srv.URL + "/fleet/ops.frag")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound {
t.Errorf("the retired fragment route still answers %d", resp.StatusCode)
}
}
// The loop actually patches. A stream that opens and then says nothing
// is indistinguishable, from the chair, from the poll it replaced being
// deleted — so this drives real traffic through the recorder the panel
// reads and waits for the panel to redraw itself.
func TestOpsStreamPatchesWhenTheForgeMoves(t *testing.T) {
srv := opsStreamServer(t)
// Traffic first: the renders ring is what the panel draws, and a
// forge with an empty window has nothing to say by design.
for i := 0; i < 3; i++ {
resp, err := http.Get(srv.URL + "/fleet/ops")
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
}
req, err := http.NewRequest("GET", srv.URL+"/fleet/ops.sse", nil)
if err != nil {
t.Fatal(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// Generous: the cadence is three seconds and a loaded host may miss
// a tick. A tight budget here buys a flaky test, not a faster one.
done := make(chan string, 1)
go func() {
sc := bufio.NewScanner(resp.Body)
sc.Buffer(make([]byte, 0, 1<<20), 1<<20)
patched := false
for sc.Scan() {
line := sc.Text()
if line == "event: datastar-patch-elements" {
patched = true
continue
}
if patched && strings.HasPrefix(line, "data: elements ") {
done <- line
return
}
}
done <- ""
}()
select {
case line := <-done:
if line == "" {
t.Fatal("the stream closed without patching")
}
if !strings.Contains(line, "<") {
t.Fatalf("patch carried no markup: %q", line)
}
case <-time.After(12 * time.Second):
t.Fatal("no patch arrived: the panel opens a stream that never redraws")
}
}