Skip to content
quarry Artifact [b83cba59e7]
Login

Artifact [b83cba59e7]

Artifact b83cba59e7fbf86ac2cd7f2c34885640f0b8dbb297efb1be41c9ebd79bd0d646:


package web

import (
	"bytes"
	"html/template"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

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

// A tab for a plane that cannot be used here is not an empty section
// but a wrong one: Chat shows only where the forge runs a chat plane
// and the repository's chat setting is on, and the page behind a hidden
// tab is a 404 rather than an empty channel.
func TestChatTabFollowsThePlaneAndTheSetting(t *testing.T) {
	ix := landingIndex(t, false)
	serve := func(chat api.ChatPlane, meta func() map[string]api.RepoMeta) *httptest.Server {
		r := ops.NewRegistry()
		deps := api.Deps{Version: "v", Chat: chat, RepoMeta: meta,
			Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}})}
		if err := api.RegisterAll(r, deps); err != nil {
			t.Fatal(err)
		}
		srv := httptest.NewServer(HandlerWith(r, nil, nil, nil))
		t.Cleanup(srv.Close)
		return srv
	}
	on := func() map[string]api.RepoMeta { return map[string]api.RepoMeta{"alpha": {Chat: true}} }
	off := func() map[string]api.RepoMeta { return map[string]api.RepoMeta{"alpha": {Chat: false}} }
	cases := []struct {
		name string
		srv  *httptest.Server
		tab  bool
	}{
		{"no chat plane", serve(nil, on), false},
		{"plane and setting on", serve(&webChatStub{}, on), true},
		{"plane, setting off", serve(&webChatStub{}, off), false},
		{"plane, no settings at all", serve(&webChatStub{}, nil), true},
	}
	for _, c := range cases {
		page := landingGet(t, c.srv, "/r/alpha/timeline")
		if got := strings.Contains(page, `href="/r/alpha/chat"`); got != c.tab {
			t.Errorf("%s: chat tab shown=%v, want %v", c.name, got, c.tab)
		}
		// Landings and CI stay: this store is its own source of truth.
		for _, tab := range []string{"/r/alpha/landing", "/r/alpha/ci"} {
			if !strings.Contains(page, `href="`+tab+`"`) {
				t.Errorf("%s: %s tab missing", c.name, tab)
			}
		}
		res, err := http.Get(c.srv.URL + "/r/alpha/chat")
		if err != nil {
			t.Fatal(err)
		}
		res.Body.Close()
		if want := map[bool]int{true: 200, false: 404}[c.tab]; res.StatusCode != want {
			t.Errorf("%s: GET /chat = %d, want %d", c.name, res.StatusCode, want)
		}
	}
}

// renderBase draws the chrome — header, repo bar, tabs — around an
// empty page, which is all a tab test needs.
func renderBase(t *testing.T, d pageData) string {
	t.Helper()
	page := template.Must(template.Must(base.Clone()).Parse(`{{define "content"}}{{end}}`))
	var buf bytes.Buffer
	if err := page.Execute(&buf, d); err != nil {
		t.Fatal(err)
	}
	return buf.String()
}

// A repository that tracks an upstream is not the source of truth:
// nothing lands, builds or is discussed here, so those tabs go and
// the rest stay.
func TestTrackingMirrorHidesLandingsCIAndChat(t *testing.T) {
	var d pageData
	d.Repo, d.Section = "alpha", "timeline"
	d.Posture.Mirror.Tracking, d.Posture.Mirror.URL, d.Posture.Chat = true, "https://x/y", true
	html := renderBase(t, d)
	for _, tab := range []string{"/r/alpha/landing", "/r/alpha/ci", "/r/alpha/chat"} {
		if strings.Contains(html, `href="`+tab+`"`) {
			t.Errorf("a tracking mirror shows %s", tab)
		}
	}
	for _, tab := range []string{"/r/alpha/timeline", "/r/alpha/ticket", "/r/alpha/forum"} {
		if !strings.Contains(html, `href="`+tab+`"`) {
			t.Errorf("a tracking mirror lost %s", tab)
		}
	}
	// Detached: quarry is the source of truth again.
	d.Posture.Mirror.Tracking, d.Posture.DetachedFrom = false, "https://x/y"
	html = renderBase(t, d)
	for _, tab := range []string{"/r/alpha/landing", "/r/alpha/ci", "/r/alpha/chat"} {
		if !strings.Contains(html, `href="`+tab+`"`) {
			t.Errorf("a detached store lacks %s", tab)
		}
	}
}

// The chat rail lists only rooms the viewer can enter: a repository
// with chat off is not a channel, and neither is one where the viewer
// holds no chat capability.
func TestChatRailListsOnlyUsableChannels(t *testing.T) {
	ix := landingIndex(t, false)
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v", Chat: &webChatStub{},
		RepoMeta: func() map[string]api.RepoMeta {
			return map[string]api.RepoMeta{"alpha": {Chat: true}, "beta": {Chat: false}, "gamma": {Chat: true}}
		},
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}, "beta": {Index: ix}, "gamma": {Index: ix}})}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	rail := func(caps Caps) string {
		srv := httptest.NewServer(HandlerWith(r, nil, nil,
			[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(caps)},
			Verify(func(string, string, string, ops.Auth) api.Decision { return api.Allowed })))
		t.Cleanup(srv.Close)
		page := asDan(t, srv, "/r/alpha/chat")
		i := strings.Index(page, `id="rail-channels"`)
		if i < 0 {
			t.Fatalf("no rail on the chat page")
		}
		return page[i : i+strings.Index(page[i:], "</ul>")]
	}
	// Chat everywhere the viewer is allowed: alpha and gamma, not beta.
	got := rail(stubCaps{letters: "C"})
	for name, want := range map[string]bool{"alpha": true, "beta": false, "gamma": true} {
		if strings.Contains(got, "/r/"+name+"/chat") != want {
			t.Errorf("with C, rail lists %s = %v, want %v", name, !want, want)
		}
	}
	// No chat capability anywhere: no channels, even where chat is on.
	if got := rail(stubCaps{letters: "o"}); strings.Contains(got, "/r/alpha/chat") || strings.Contains(got, "/r/gamma/chat") {
		t.Errorf("without C the rail still lists channels: %s", got)
	}
}

// Accepting a landing merges on the store and can take a while: with
// script the decision form goes busy on submit, so nothing takes a
// second press and the pressed control says what it is doing. Read
// from the template source, which is what the page is made of.
func TestLandingDecisionFormGoesBusyOnSubmit(t *testing.T) {
	src := landingSource
	for _, want := range []string{
		`data-on:submit="$busy = true"`,
		`data-class:busy="$busy"`,
		`data-class:is-busy="$busy && $pressed == 'accept'"`,
		`<span class="b-busy">Accepting…</span>`,
		`data-class:show="$busy"`,
	} {
		if !strings.Contains(src, want) {
			t.Errorf("landing decision form lacks %s", want)
		}
	}
}