app/atom_test.go
107 lines · 3323 bytes
1package app
2
3import (
4 "encoding/xml"
5 "net/http/httptest"
6 "net/url"
7 "strings"
8 "testing"
9 "time"
10
11 "github.com/krazywarez/devianter"
12)
13
14// atomFeed is the subset of an Atom document the tests check. Decoding through
15// encoding/xml also proves the output is well-formed XML.
16type atomFeed struct {
17 XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
18 ID string `xml:"id"`
19 Updated string `xml:"updated"`
20 Links []struct {
21 Rel string `xml:"rel,attr"`
22 Href string `xml:"href,attr"`
23 } `xml:"link"`
24 Entries []struct {
25 ID string `xml:"id"`
26 Published string `xml:"published"`
27 Updated string `xml:"updated"`
28 Thumbnail struct {
29 URL string `xml:"url,attr"`
30 } `xml:"http://search.yahoo.com/mrss/ group>thumbnail"`
31 } `xml:"entry"`
32}
33
34func renderFeed(t *testing.T, devs []devianter.Deviation) atomFeed {
35 t.Helper()
36 rec := httptest.NewRecorder()
37 s := skunkyart{Writer: rec, Host: "http://localhost", Atom: true, _pth: "/dd", Args: url.Values{"atom": {"true"}}}
38 s.DeviationList(devs, true)
39
40 var feed atomFeed
41 if err := xml.Unmarshal(rec.Body.Bytes(), &feed); err != nil {
42 t.Fatalf("feed is not well-formed: %v\n%s", err, rec.Body.String())
43 }
44 return feed
45}
46
47func selfLink(f atomFeed) string {
48 for _, l := range f.Links {
49 if l.Rel == "self" {
50 return l.Href
51 }
52 }
53 return ""
54}
55
56// TestAtomFeedHasTheRequiredElements pins what the spec demands and readers
57// reject a feed without: a feed id and updated time, IRI entry ids, RFC 3339
58// timestamps, and the correctly spelled media thumbnail.
59func TestAtomFeedHasTheRequiredElements(t *testing.T) {
60 proxy, nsfw := CFG.Proxy, CFG.Nsfw
61 CFG.Proxy, CFG.Nsfw = true, true
62 defer func() { CFG.Proxy, CFG.Nsfw = proxy, nsfw }()
63
64 d := *fullviewDeviation()
65 d.ID = 123
66 d.Title = "T"
67 d.Author.Username = "alice"
68 d.PublishedTime.Time = time.Date(2026, 8, 11, 18, 36, 34, 0, time.UTC)
69
70 feed := renderFeed(t, []devianter.Deviation{d})
71
72 if !strings.HasPrefix(feed.ID, "http://localhost/dd?") {
73 t.Errorf("feed id %q, want the feed's own URL", feed.ID)
74 }
75 if feed.Updated != "2026-08-11T18:36:34Z" {
76 t.Errorf("feed updated %q, want the newest entry's RFC 3339 time", feed.Updated)
77 }
78 if selfLink(feed) != feed.ID {
79 t.Errorf("self link %q, want %q", selfLink(feed), feed.ID)
80 }
81 if len(feed.Entries) != 1 {
82 t.Fatalf("%d entries, want 1", len(feed.Entries))
83 }
84 e := feed.Entries[0]
85 if !strings.HasPrefix(e.ID, "http://localhost/post/alice/") {
86 t.Errorf("entry id %q, want the post's URL", e.ID)
87 }
88 if e.Published != "2026-08-11T18:36:34Z" || e.Updated != e.Published {
89 t.Errorf("entry published %q updated %q, want RFC 3339 and equal", e.Published, e.Updated)
90 }
91 if e.Thumbnail.URL == "" {
92 t.Error("entry has no media:thumbnail, want one (the element used to be misspelled)")
93 }
94}
95
96// TestAtomFeedIsValidWhenEmpty pins that an empty listing still yields a feed
97// with an updated time, so a reader polling an empty gallery gets a document
98// rather than an error.
99func TestAtomFeedIsValidWhenEmpty(t *testing.T) {
100 feed := renderFeed(t, nil)
101 if feed.ID == "" || feed.Updated == "" {
102 t.Errorf("empty feed id %q updated %q, want both set", feed.ID, feed.Updated)
103 }
104 if _, err := time.Parse(time.RFC3339, feed.Updated); err != nil {
105 t.Errorf("empty feed updated %q is not RFC 3339: %v", feed.Updated, err)
106 }
107}