Skip to content
quarry Artifact [8381cec5dc]
Login

Artifact [8381cec5dc]

Artifact 8381cec5dc0e9f90c26a100e6655540cb058d45cbdd36711d8c5ec84b501f3c2:


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 := 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")
	}
}