Skip to content
quarry Artifact [c57dc3dfa9]
Login

Artifact [c57dc3dfa9]

Artifact c57dc3dfa980eb792520b8639b76e7b29d80e1bd48d618dd29ec5d6951585c8f:


package web

import (
	"context"
	"net/http"
	"net/http/httptest"
	"strings"
	"sync"
	"testing"

	"craftdesign.group/quarry/internal/ops"
)

type dashFeedIn struct {
	Kind   string `json:"kind"`
	Find   string `json:"find"`
	Repo   string `json:"repo"`
	Offset int    `json:"offset"`
}

type dashReposIn struct {
	Find string `json:"find"`
}

type dashAcrossIn struct {
	Awaiting bool   `json:"awaiting"`
	Mine     bool   `json:"mine"`
	Brief    bool   `json:"brief"`
	State    string `json:"state"`
	Limit    int    `json:"limit"`
}

type dashTeamsIn struct {
	Mine bool `json:"mine"`
}

// homeHarness is the home page over test doubles that count what the
// page asks for. assigned is what ticket.across answers for the reader's
// own queue; every other ticket question answers one open ticket.
type homeHarness struct {
	h      http.Handler
	mu     sync.Mutex
	feeds  int
	across []dashAcrossIn
}

func newHomeHarness(t *testing.T, assigned int) *homeHarness {
	t.Helper()
	hh := &homeHarness{}
	r := ops.NewRegistry()
	type empty struct{}
	type tickets struct {
		Tickets []map[string]any `json:"tickets"`
	}
	must := func(err error) {
		t.Helper()
		if err != nil {
			t.Fatal(err)
		}
	}
	must(ops.Register(r, ops.Op[dashFeedIn, struct {
		Total int `json:"total"`
	}]{Name: "me.feed", Doc: "test double", Kind: ops.Query, Auth: ops.Token, Route: "GET /api/v1/me/feed",
		Run: func(context.Context, dashFeedIn) (struct {
			Total int `json:"total"`
		}, error) {
			hh.mu.Lock()
			hh.feeds++
			hh.mu.Unlock()
			return struct {
				Total int `json:"total"`
			}{}, nil
		}}))
	must(ops.Register(r, ops.Op[dashReposIn, struct {
		Repos []map[string]any `json:"repos"`
	}]{Name: "me.repos", Doc: "test double", Kind: ops.Query, Auth: ops.Token, Route: "GET /api/v1/me/repos",
		Run: func(context.Context, dashReposIn) (struct {
			Repos []map[string]any `json:"repos"`
		}, error) {
			return struct {
				Repos []map[string]any `json:"repos"`
			}{[]map[string]any{{"name": "alpha", "role": "admin"}}}, nil
		}}))
	must(ops.Register(r, ops.Op[dashAcrossIn, tickets]{Name: "ticket.across", Doc: "test double", Kind: ops.Query, Auth: ops.Token, Route: "GET /api/v1/me/tickets",
		Run: func(_ context.Context, in dashAcrossIn) (tickets, error) {
			hh.mu.Lock()
			hh.across = append(hh.across, in)
			hh.mu.Unlock()
			n := 1
			if in.Mine {
				n = assigned
			}
			var out tickets
			for i := 0; i < n; i++ {
				out.Tickets = append(out.Tickets, map[string]any{"repo": "alpha", "id": "abcdef0123456789",
					"title": "a ticket", "status": "Open", "number": i + 1, "mine": in.Mine})
			}
			return out, nil
		}}))
	must(ops.Register(r, ops.Op[dashTeamsIn, struct {
		Teams []struct{} `json:"teams"`
	}]{Name: "team.list", Doc: "test double", Kind: ops.Query, Auth: ops.Token, Route: "GET /api/v1/teams",
		Run: func(context.Context, dashTeamsIn) (struct {
			Teams []struct{} `json:"teams"`
		}, error) {
			return struct {
				Teams []struct{} `json:"teams"`
			}{}, nil
		}}))
	must(ops.Register(r, ops.Op[empty, struct {
		Version string `json:"version"`
	}]{Name: "meta.version", Doc: "test double", Kind: ops.Query, Auth: ops.Anonymous, Route: "GET /api/v1/version",
		Run: func(context.Context, empty) (struct {
			Version string `json:"version"`
		}, error) {
			return struct {
				Version string `json:"version"`
			}{"t"}, nil
		}}))
	hh.h = HandlerWith(r, nil, nil, []Option{WithSessions(fakeSessions{user: "jan", roles: map[string]ops.Auth{"": ops.Token}})})
	return hh
}

// get answers path as the signed-in reader, with what it asked for
// since the last call.
func (hh *homeHarness) get(t *testing.T, path string) (int, string, int, []dashAcrossIn) {
	t.Helper()
	hh.mu.Lock()
	hh.feeds, hh.across = 0, nil
	hh.mu.Unlock()
	req := httptest.NewRequest("GET", path, nil)
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	w := httptest.NewRecorder()
	hh.h.ServeHTTP(w, req)
	hh.mu.Lock()
	defer hh.mu.Unlock()
	return w.Code, w.Body.String(), hh.feeds, hh.across
}

// The home page is where the reader works — repositories, teams,
// tickets — and none of it waits on the activity feed, which reads every
// event of every tracker the reader reaches. The feed follows the page
// in, behind a skeleton only script can show; without script the page
// links to the activity page instead.
func TestTheHomePageLeavesTheFeedForLater(t *testing.T) {
	hh := newHomeHarness(t, 0)
	code, body, feeds, across := hh.get(t, "/")
	if code != http.StatusOK {
		t.Fatalf("FAIL: / answered %d", code)
	}
	if feeds != 0 {
		t.Errorf("FAIL: the home page asked for the feed %d times", feeds)
	}
	for _, want := range []string{`data-hook="dash-repos"`, `data-hook="dash-tickets"`,
		`data-init="@get('/activity?frag=recent')"`, `<noscript><p class="dash-more"><a href="/activity">`} {
		if !strings.Contains(body, want) {
			t.Errorf("FAIL: the home page has no %s", want)
		}
	}
	// The skeleton is hidden until Datastar shows it.
	if !strings.Contains(body, `style="display: none" data-show="true" aria-hidden="true" data-hook="dash-skeleton"`) {
		t.Errorf("FAIL: the skeleton shows without script")
	}
	for _, in := range across {
		if !in.Brief || in.Awaiting {
			t.Errorf("FAIL: the home page asked ticket.across for more than a brief list: %+v", in)
		}
	}
	// The rows arrive as one patch of the section, from the feed alone.
	code, body, feeds, across = hh.get(t, "/activity?frag=recent")
	if code != http.StatusOK || feeds != 1 || len(across) != 0 || !strings.Contains(body, `data-hook="dash-activity"`) {
		t.Errorf("FAIL: ?frag=recent: %d, %d feeds, %d ticket asks", code, feeds, len(across))
	}
	// The band of tickets waiting on you leads the activity page's ticket
	// changes, and is asked for there only.
	if _, _, _, across = hh.get(t, "/activity?kind=t"); len(across) != 1 || !across[0].Awaiting {
		t.Errorf("FAIL: ticket changes did not ask for the waiting band: %+v", across)
	}
	if _, _, _, across = hh.get(t, "/activity?kind=c"); len(across) != 0 {
		t.Errorf("FAIL: check-ins asked for the waiting band")
	}
}

// The feed as it was addressed on the home page arrives at the activity
// page, its filters kept.
func TestTheOldFeedAddressesArriveAtTheActivityPage(t *testing.T) {
	hh := newHomeHarness(t, 0)
	for path, want := range map[string]string{
		"/?kind=t":           "/activity?kind=t",
		"/?offset=10&repo=x": "/activity?offset=10&repo=x",
		"/?frag=1&kind=w":    "/activity?kind=w",
	} {
		req := httptest.NewRequest("GET", path, nil)
		req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
		w := httptest.NewRecorder()
		hh.h.ServeHTTP(w, req)
		if w.Code != http.StatusSeeOther || w.Header().Get("Location") != want {
			t.Errorf("FAIL: %s answered %d to %q, want %s", path, w.Code, w.Header().Get("Location"), want)
		}
	}
}

// The tickets are the reader's own queue when they have one, and what is
// open across their trackers when nothing is assigned to them.
func TestTheHomeTicketsAreYoursOrWhatIsOpen(t *testing.T) {
	_, body, _, _ := newHomeHarness(t, 2).get(t, "/")
	if !strings.Contains(body, `data-mine="true"`) || strings.Count(body, `data-hook="dash-ticket"`) != 2 {
		t.Errorf("FAIL: the reader's two assigned tickets are not the list")
	}
	_, body, _, _ = newHomeHarness(t, 0).get(t, "/")
	if !strings.Contains(body, `data-mine="false"`) || strings.Count(body, `data-hook="dash-ticket"`) != 1 {
		t.Errorf("FAIL: with nothing assigned, the open tickets are not the list")
	}
}