package web
import (
"encoding/xml"
"net/http"
"strings"
"testing"
)
// A visitor who cannot sign in can follow a repository (7a608b5b): the
// timeline is an Atom feed whose items link where the timeline rows
// do, it keeps the page's filters, and the pages announce it.
func TestAVisitorCanFollowARepositoryTimeline(t *testing.T) {
srv := activityServer(t)
resp, err := http.Get(srv.URL + "/r/alpha/timeline.atom")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 || !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/atom+xml") {
t.Fatalf("FAIL: status %d, type %q", resp.StatusCode, resp.Header.Get("Content-Type"))
}
var f atomFeed
if err := xml.NewDecoder(resp.Body).Decode(&f); err != nil {
t.Fatalf("FAIL: not Atom: %v", err)
}
if len(f.Entries) != 4 || f.Updated != "2026-08-04T16:00:00Z" {
t.Fatalf("FAIL: %d entries updated %q, want 4 and the newest check-in's time", len(f.Entries), f.Updated)
}
ids := map[string]bool{}
for _, e := range f.Entries {
if !strings.HasPrefix(e.Link.Href, srv.URL+"/r/alpha/checkin/") || e.Author.Name == "" || ids[e.ID] {
t.Fatalf("FAIL: entry %+v: link must be an absolute check-in URL, author named, id unique", e)
}
ids[e.ID] = true
}
_, all := getPage(t, srv, "/r/alpha/timeline.atom")
_, feat := getPage(t, srv, "/r/alpha/timeline.atom?branch=feat")
if strings.Count(feat, "<entry>") >= strings.Count(all, "<entry>") || !strings.Contains(feat, "timeline.atom?branch=feat") {
t.Fatal("FAIL: a branch feed should narrow the entries and name itself with the filter")
}
_, page := getPage(t, srv, "/r/alpha/timeline?branch=feat")
if !strings.Contains(page, `rel="alternate" type="application/atom+xml"`) {
t.Fatal("FAIL: the page head does not announce the feed")
}
if !strings.Contains(page, `data-hook="timeline-feed"`) || !strings.Contains(page, `/r/alpha/timeline.atom?branch=feat`) {
t.Fatal("FAIL: the timeline's feed link does not carry the branch filter")
}
t.Logf("ok: 4 entries, newest %s; branch feed %d of %d; page announces and links it",
f.Updated, strings.Count(feat, "<entry>"), strings.Count(all, "<entry>"))
}
// A feed item says what kind of event it is exactly once: the stored
// comment of a ticket or landing already starts with the word.
func TestAFeedItemNamesItsKindOnce(t *testing.T) {
cases := []struct{ kind, head, want string }{
{"t", "Ticket [bf5fbed5d6] Mirror admin", "Ticket [bf5fbed5d6] Mirror admin"},
{"l", "Landing accepted: 7a608b5b", "Landing accepted: 7a608b5b"},
{"lc", "Landing comment on 7a608b5b", "Landing comment on 7a608b5b"},
{"w", "Changes to wiki page Home", "Wiki: Changes to wiki page Home"},
{"e", "Technote: release 1.0", "Note: release 1.0"},
{"f", "Re: build fails", "Forum: Re: build fails"},
{"ci", "fix the parser", "fix the parser"},
}
for _, c := range cases {
if got := feedTitle(c.kind, c.head); got != c.want {
t.Errorf("FAIL %s: %q, want %q", c.kind, got, c.want)
}
}
t.Log("ok: tickets and landings keep their own word; wiki, notes and forum are labelled; check-ins bare")
}
// Two changes to one ticket are two different feed entries to a reader.
func TestAFeedTicketEntrySaysWhatChanged(t *testing.T) {
base := timelineEntry{Kind: "t", Comment: "Ticket [2ee2bc83b1] held-file probe"}
opened, closed := base, base
opened.Change = "opened"
closed.Change = "status Closed, resolution Fixed"
h1, s1 := feedText(opened)
h2, s2 := feedText(closed)
if h1 == h2 || s1 == s2 {
t.Fatalf("FAIL: two changes read alike: %q / %q", h1, h2)
}
if h2 != "Ticket [2ee2bc83b1] held-file probe — status Closed, resolution Fixed" {
t.Errorf("FAIL: %q", h2)
}
if h, _ := feedText(base); h != "Ticket [2ee2bc83b1] held-file probe" {
t.Errorf("FAIL: a row with no change summary grew a suffix: %q", h)
}
}