package chrome import ( "html/template" "net/http/httptest" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vaughan0/go-ini" ) // testConf synthesizes the shared instance config the way the services read // it: one section per service plus the [sr.ht] block. func testConf() ini.File { return ini.File{ "sr.ht": { "site-name": "srht.example", "environment": "production", }, "meta.sr.ht": {"origin": "https://meta.example"}, "git.sr.ht": {"origin": "https://git.example"}, "todo.sr.ht": {"origin": "https://todo.example"}, "builds.sr.ht": {"origin": "https://builds.example"}, "hub.sr.ht": {"origin": "https://hub.example"}, "paste.sr.ht": {"origin": "https://paste.example"}, "pages.sr.ht": {"origin": "https://pages.example"}, "compare.sr.ht": {"origin": "https://compare.example"}, "dolt.sr.ht": {"origin": "https://dolt.example"}, "ghost.sr.ht": {}, // no origin -> must not appear "webhooks": {"private-key": "x"}, } } func TestBuildNavOrderExclusionsActive(t *testing.T) { nav := BuildNav(testConf(), "compare.sr.ht") var names []string for _, it := range nav { names = append(names, it.Name) } // Canonical services first in canonical order, customs alphabetical after; // hub/paste/pages and the origin-less section excluded. assert.Equal(t, []string{"git", "todo", "builds", "meta", "compare", "dolt"}, names) for _, it := range nav { assert.Equal(t, it.Name == "compare", it.Active, "active flag for %s", it.Name) } } func TestPageURLsAndIdentity(t *testing.T) { svc := NewService(testConf(), "compare.sr.ht") r := httptest.NewRequest("GET", "/~alice/demo?a=1", nil) p := svc.Page(r, "t", "alice") assert.Equal(t, "srht.example", p.SiteName) assert.Equal(t, "compare", p.SiteLabel) assert.Equal(t, "https://meta.example/login?return_to="+ "https%3A%2F%2Fcompare.example%2F~alice%2Fdemo%3Fa%3D1", p.LoginURL) assert.Equal(t, "https://meta.example/logout?return_to="+ "https%3A%2F%2Fcompare.example", p.LogoutURL) // Hub is configured, so the profile link prefers hub's ~username page. assert.Equal(t, "https://hub.example/~alice", p.ProfileURL) assert.Equal(t, "container", p.ContainerClass) assert.False(t, p.ShowBanner, "production must not show the env banner") } func TestPageProfileFallsBackToMeta(t *testing.T) { conf := testConf() delete(conf, "hub.sr.ht") svc := NewService(conf, "compare.sr.ht") r := httptest.NewRequest("GET", "/", nil) assert.Equal(t, "https://meta.example/profile", svc.Page(r, "t", "alice").ProfileURL) // Anonymous viewers get the meta profile link regardless of hub. assert.Equal(t, "https://meta.example/profile", NewService(testConf(), "compare.sr.ht").Page(r, "t", "").ProfileURL) } func TestPageEnvBanner(t *testing.T) { conf := testConf() conf["sr.ht"]["environment"] = "staging" svc := NewService(conf, "compare.sr.ht") p := svc.Page(httptest.NewRequest("GET", "/", nil), "t", "") assert.True(t, p.ShowBanner) assert.Equal(t, "STAGING", p.Environment) } // render executes a minimal layout that invokes both shared partials against v. func render(t *testing.T, v any) string { t.Helper() tpl := template.New("layout") tpl = MustAttach(tpl) tpl, err := tpl.Parse(`{{template "srht-env-banner" .}}`) require.NoError(t, err) var b strings.Builder require.NoError(t, tpl.Execute(&b, v)) return b.String() } func TestNavTemplateLoggedIn(t *testing.T) { svc := NewService(testConf(), "compare.sr.ht") svc.ExtraNav = []NavItem{{Name: "tokens", Origin: "/tokens"}} p := svc.Page(httptest.NewRequest("GET", "/", nil), "t", "alice") out := render(t, p) assert.Contains(t, out, "icon icon-circle") // The brand is two links: the site name to hub, the red label to us. assert.Contains(t, out, `srht.example`) assert.Contains(t, out, `compare`) assert.Contains(t, out, `href="https://git.example"`) assert.Contains(t, out, `href="/tokens"`, "extra nav entries must render") assert.Contains(t, out, "Logged in as") assert.NotContains(t, out, "ENVIRONMENT") } func TestNavTemplateAnonymous(t *testing.T) { svc := NewService(testConf(), "compare.sr.ht") p := svc.Page(httptest.NewRequest("GET", "/", nil), "t", "") out := render(t, p) // The switcher renders only for authenticated viewers. assert.NotContains(t, out, `href="https://git.example"`) assert.Contains(t, out, "Log in") assert.Contains(t, out, "Register") } // TestNavBrandWithoutHub covers the instance that runs no hub: the site name // has nowhere else to point, so it falls back to the service root and the // brand becomes two links to the same place rather than a dead one. func TestNavBrandWithoutHub(t *testing.T) { conf := testConf() delete(conf, "hub.sr.ht") svc := NewService(conf, "compare.sr.ht") out := render(t, svc.Page(httptest.NewRequest("GET", "/", nil), "t", "alice")) assert.Contains(t, out, `srht.example`) assert.Contains(t, out, `compare`) } // TestNavTemplateEmbeddedPage guards the documented consumption pattern: a // service view struct embedding Page resolves the promoted fields. func TestNavTemplateEmbeddedPage(t *testing.T) { type viewData struct { Page Data any } svc := NewService(testConf(), "dolt.sr.ht") v := viewData{Page: svc.Page(httptest.NewRequest("GET", "/", nil), "t", "alice")} out := render(t, v) assert.Contains(t, out, `dolt`) assert.Contains(t, out, "Logged in as") } // renderList executes the srht-repo-list partial alone against v. func renderList(t *testing.T, v RepoList) string { t.Helper() tpl := MustAttach(template.New("t")) tpl, err := tpl.Parse(`{{template "srht-repo-list" .}}`) require.NoError(t, err) var b strings.Builder require.NoError(t, tpl.Execute(&b, v)) return b.String() } func TestRepoListCards(t *testing.T) { out := renderList(t, RepoList{Items: []ListItem{ {Href: "/~a/pub", Title: "~a/pub", Visibility: "PUBLIC", Description: "described"}, {Href: "/~a/unl", Title: "~a/unl", Visibility: "UNLISTED"}, {Href: "/~a/prv", Title: "~a/prv", Visibility: "PRIVATE"}, {Href: "/~a/none", Title: "~a/none"}, // service without visibility (spec spaces) }}) assert.Equal(t, 4, strings.Count(out, `class="event"`)) assert.Contains(t, out, "

described

") // PUBLIC and empty visibility render no label; the others render lowercase. assert.Equal(t, 1, strings.Count(out, ">unlisted")) assert.Equal(t, 1, strings.Count(out, ">private")) assert.Equal(t, 2, strings.Count(out, "pull-right")) } // TestOptionalColumnsRenderOnlyWhenCarried is the rule that keeps all four // listing services consumers: dolt has no timestamp in its schema, bench and // spec do, and neither may force a column of blanks on the other. func TestOptionalColumnsRenderOnlyWhenCarried(t *testing.T) { at := time.Now().Add(-3 * time.Hour) bare := renderList(t, RepoList{Items: []ListItem{{Href: "/a", Title: "~a/db"}}}) assert.NotContains(t, bare, "text-muted\">\n", "no empty metadata line") assert.NotContains(t, bare, "ago") rich := renderList(t, RepoList{Items: []ListItem{ {Href: "/a", Title: "~a/repo", Updated: at, Meta: []string{"12 runs"}}, }}) assert.Contains(t, rich, "3 hours ago") assert.Contains(t, rich, AbsTime(at), "the exact stamp rides in the title") assert.Contains(t, rich, "12 runs") } // renderTable executes the srht-repo-table partial alone against v. func renderTable(t *testing.T, v RepoList) string { t.Helper() tpl := MustAttach(template.New("t")) tpl, err := tpl.Parse(`{{template "srht-repo-table" .}}`) require.NoError(t, err) var b strings.Builder require.NoError(t, tpl.Execute(&b, v)) return b.String() } func TestRepoTableIsTheSameDotAsTheCards(t *testing.T) { at := time.Now().Add(-2 * 24 * time.Hour) list := RepoList{ Items: []ListItem{ {Href: "/a", Title: "~a/one", Visibility: "PRIVATE", Updated: at}, {Href: "/b", Title: "~a/two", Description: "described"}, }, Empty: "No repositories yet.", } out := renderTable(t, list) assert.Equal(t, 2, strings.Count(out, "")) assert.Contains(t, out, `~a/one`) assert.Contains(t, out, ">private") assert.Contains(t, out, "2 days ago") assert.Contains(t, out, "described") // The second row carries no time, so it grows no cell for one. assert.Equal(t, 1, strings.Count(out, "text-right")) assert.Contains(t, renderTable(t, RepoList{Empty: "No repositories yet."}), "No repositories yet.") } func TestRepoListEmptyState(t *testing.T) { out := renderList(t, RepoList{Empty: "No databases yet."}) assert.NotContains(t, out, "event-list") assert.Contains(t, out, "No databases yet.") } // TestLoginURLForMatchesTheNav guards the whole reason the accessor exists: a // handler redirecting to login must land the viewer exactly where the nav's // "Log in" would have. func TestLoginURLForMatchesTheNav(t *testing.T) { svc := NewService(testConf(), "compare.sr.ht") r := httptest.NewRequest("GET", "/~alice/demo?a=1", nil) assert.Equal(t, svc.Page(r, "t", "").LoginURL, svc.LoginURLFor(r)) assert.Equal(t, "https://meta.example/login?return_to="+ "https%3A%2F%2Fcompare.example%2F~alice%2Fdemo%3Fa%3D1", svc.LoginURLFor(r)) } // TestHeadLinksAreGuardedAndTheFaviconSurvivesEscaping covers the two ways the // head links go wrong: an empty href that re-requests the page, and a data: // URI that html/template rewrites to #ZgotmplZ unless it is a template.URL. func TestHeadLinksAreGuardedAndTheFaviconSurvivesEscaping(t *testing.T) { renderHead := func(t *testing.T, p Page) string { t.Helper() tpl := MustAttach(template.New("h")) tpl, err := tpl.Parse(`{{template "srht-head-links" .}}`) require.NoError(t, err) var b strings.Builder require.NoError(t, tpl.Execute(&b, p)) return b.String() } svc := NewService(testConf(), "compare.sr.ht") svc.StyleHref = "/static/main.min.0badc0de.css" out := renderHead(t, svc.Page(httptest.NewRequest("GET", "/", nil), "t", "")) assert.Contains(t, out, ``) // The "+" of image/svg+xml renders as the HTML entity +, which the // browser decodes back before the href is ever parsed as a URL. assert.Contains(t, out, `