402 lines
12 KiB
Go
402 lines
12 KiB
Go
package fa
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
)
|
|
|
|
// fakeSubmissionPage builds a minimal /view/{id}/ response that has either
|
|
// a Fav or Unfav anchor (controlled by faved). Sufficient to satisfy the
|
|
// classifySystemMessage check (title is non-System-Error) and the
|
|
// findFavLinks selector.
|
|
func fakeSubmissionPage(id int, faved bool) string {
|
|
prefix := "/fav/"
|
|
label := "+Fav"
|
|
if faved {
|
|
prefix = "/unfav/"
|
|
label = "-Fav"
|
|
}
|
|
href := prefix + strconv.Itoa(id) + "/?key=k1"
|
|
return `<html><head><title>Test submission</title></head><body>
|
|
<div class="submission-description-artist">
|
|
<a href="/user/x/"><img class="avatar" src="/a.png"/></a>
|
|
<div>
|
|
<div class="submission-title"><h2>T</h2></div>
|
|
<div><span class="c-usernameBlockSimple__displayName" title="x">x</span></div>
|
|
</div>
|
|
</div>
|
|
<a class="button" href="` + href + `">` + label + `</a>
|
|
</body></html>`
|
|
}
|
|
|
|
func fakeUserPage(name string, watching bool) string {
|
|
href := "/watch/" + name + "/?key=k2"
|
|
text := "Watch"
|
|
if watching {
|
|
href = "/unwatch/" + name + "/?key=k2"
|
|
text = "Unwatch"
|
|
}
|
|
return `<html><head><title>Userpage</title></head><body>
|
|
<div class="username"><h2><span>` + name + `</span></h2></div>
|
|
<a id="watch-button" class="button" href="` + href + `">` + text + `</a>
|
|
</body></html>`
|
|
}
|
|
|
|
func TestFav_FetchesAndFollowsFavLink(t *testing.T) {
|
|
var hits atomic.Int32
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/view/123/", func(w http.ResponseWriter, r *http.Request) {
|
|
hits.Add(1)
|
|
_, _ = w.Write([]byte(fakeSubmissionPage(123, false)))
|
|
})
|
|
mux.HandleFunc("/fav/123/", func(w http.ResponseWriter, r *http.Request) {
|
|
hits.Add(1)
|
|
if r.URL.Query().Get("key") != "k1" {
|
|
t.Errorf("missing key on /fav/: %q", r.URL.RawQuery)
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
if err := client.Fav(context.Background(), 123); err != nil {
|
|
t.Fatalf("Fav: %v", err)
|
|
}
|
|
if hits.Load() != 2 {
|
|
t.Errorf("hits = %d; want 2 (fetch + follow)", hits.Load())
|
|
}
|
|
}
|
|
|
|
func TestFav_AlreadyFavedIsNoOp(t *testing.T) {
|
|
var follows atomic.Int32
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/view/77/", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(fakeSubmissionPage(77, true)))
|
|
})
|
|
mux.HandleFunc("/fav/77/", func(w http.ResponseWriter, r *http.Request) {
|
|
follows.Add(1)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
if err := client.Fav(context.Background(), 77); err != nil {
|
|
t.Fatalf("Fav: %v", err)
|
|
}
|
|
if follows.Load() != 0 {
|
|
t.Errorf("/fav/ was hit %d times; want 0 (already faved)", follows.Load())
|
|
}
|
|
}
|
|
|
|
func TestUnfav_FollowsUnfavLink(t *testing.T) {
|
|
var hits atomic.Int32
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/view/55/", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(fakeSubmissionPage(55, true)))
|
|
})
|
|
mux.HandleFunc("/unfav/55/", func(w http.ResponseWriter, r *http.Request) {
|
|
hits.Add(1)
|
|
if r.URL.Query().Get("key") != "k1" {
|
|
t.Errorf("missing key on /unfav/")
|
|
}
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
if err := client.Unfav(context.Background(), 55); err != nil {
|
|
t.Fatalf("Unfav: %v", err)
|
|
}
|
|
if hits.Load() != 1 {
|
|
t.Errorf("/unfav/ hits = %d; want 1", hits.Load())
|
|
}
|
|
}
|
|
|
|
func TestWatch_FollowsWatchLink(t *testing.T) {
|
|
var hits atomic.Int32
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/user/alice/", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(fakeUserPage("alice", false)))
|
|
})
|
|
mux.HandleFunc("/watch/alice/", func(w http.ResponseWriter, r *http.Request) {
|
|
hits.Add(1)
|
|
if r.URL.Query().Get("key") != "k2" {
|
|
t.Errorf("missing key on /watch/")
|
|
}
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
if err := client.Watch(context.Background(), "alice"); err != nil {
|
|
t.Fatalf("Watch: %v", err)
|
|
}
|
|
if hits.Load() != 1 {
|
|
t.Errorf("/watch/ hits = %d", hits.Load())
|
|
}
|
|
}
|
|
|
|
func TestUnwatch_AlreadyNotWatching_NoOp(t *testing.T) {
|
|
var follows atomic.Int32
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/user/bob/", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(fakeUserPage("bob", false)))
|
|
})
|
|
mux.HandleFunc("/unwatch/bob/", func(w http.ResponseWriter, r *http.Request) {
|
|
follows.Add(1)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
if err := client.Unwatch(context.Background(), "bob"); err != nil {
|
|
t.Fatalf("Unwatch: %v", err)
|
|
}
|
|
if follows.Load() != 0 {
|
|
t.Errorf("/unwatch/ was hit; want no-op when not watching")
|
|
}
|
|
}
|
|
|
|
func TestPostSubmissionComment_POSTsCorrectForm(t *testing.T) {
|
|
var got url.Values
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/view/200/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "want POST", http.StatusBadRequest)
|
|
return
|
|
}
|
|
body, _ := io.ReadAll(r.Body)
|
|
got, _ = url.ParseQuery(string(body))
|
|
_, _ = w.Write([]byte("<html><body><div class='comment-container'>posted</div></body></html>"))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
err := client.PostSubmissionComment(context.Background(), 200, "Nice work!", PostCommentOptions{})
|
|
if err != nil {
|
|
t.Fatalf("PostSubmissionComment: %v", err)
|
|
}
|
|
if got.Get("action") != "reply" {
|
|
t.Errorf("action = %q", got.Get("action"))
|
|
}
|
|
if got.Get("reply") != "Nice work!" {
|
|
t.Errorf("reply = %q", got.Get("reply"))
|
|
}
|
|
if got.Get("replyto") != "" {
|
|
t.Errorf("replyto should be empty for top-level; got %q", got.Get("replyto"))
|
|
}
|
|
}
|
|
|
|
func TestPostSubmissionComment_ReplytoSetForThreadedReply(t *testing.T) {
|
|
var got url.Values
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/view/200/", func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
got, _ = url.ParseQuery(string(body))
|
|
_, _ = w.Write([]byte("<html><body>ok</body></html>"))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
err := client.PostSubmissionComment(context.Background(), 200, "reply text", PostCommentOptions{ParentID: 999})
|
|
if err != nil {
|
|
t.Fatalf("PostSubmissionComment: %v", err)
|
|
}
|
|
if got.Get("replyto") != "999" {
|
|
t.Errorf("replyto = %q; want 999", got.Get("replyto"))
|
|
}
|
|
}
|
|
|
|
func TestPostJournalComment_TargetsJournalURL(t *testing.T) {
|
|
var hit string
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/journal/400/", func(w http.ResponseWriter, r *http.Request) {
|
|
hit = r.URL.Path
|
|
_, _ = w.Write([]byte("<html><body>ok</body></html>"))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
err := client.PostJournalComment(context.Background(), 400, "hi", PostCommentOptions{})
|
|
if err != nil {
|
|
t.Fatalf("PostJournalComment: %v", err)
|
|
}
|
|
if hit != "/journal/400/" {
|
|
t.Errorf("hit = %q", hit)
|
|
}
|
|
}
|
|
|
|
func TestSendNote_ScrapesKeyAndPosts(t *testing.T) {
|
|
inboxHTML := `<html><head><title>Inbox</title></head><body>
|
|
<form action="/msg/send/"><input type="hidden" name="key" value="SCRAPED_KEY"/></form>
|
|
</body></html>`
|
|
var sendBody url.Values
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/msg/pms/", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(inboxHTML))
|
|
})
|
|
mux.HandleFunc("/msg/send/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("want POST, got %s", r.Method)
|
|
}
|
|
body, _ := io.ReadAll(r.Body)
|
|
sendBody, _ = url.ParseQuery(string(body))
|
|
_, _ = w.Write([]byte("<html><body>sent</body></html>"))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
err := client.SendNote(context.Background(), "vampexx", "Re: hi", "body text")
|
|
if err != nil {
|
|
t.Fatalf("SendNote: %v", err)
|
|
}
|
|
if sendBody.Get("key") != "SCRAPED_KEY" {
|
|
t.Errorf("key = %q; want SCRAPED_KEY", sendBody.Get("key"))
|
|
}
|
|
if sendBody.Get("to") != "vampexx" {
|
|
t.Errorf("to = %q", sendBody.Get("to"))
|
|
}
|
|
if sendBody.Get("subject") != "Re: hi" {
|
|
t.Errorf("subject = %q", sendBody.Get("subject"))
|
|
}
|
|
if sendBody.Get("message") != "body text" {
|
|
t.Errorf("message = %q", sendBody.Get("message"))
|
|
}
|
|
}
|
|
|
|
func TestSendNote_NoKeyOnInboxPage_Unauthorized(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/msg/pms/", func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`<html><head><title>Inbox</title></head><body>no form here</body></html>`))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
err := client.SendNote(context.Background(), "x", "s", "b")
|
|
if !errors.Is(err, ErrUnauthorized) {
|
|
t.Fatalf("got %v; want ErrUnauthorized", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildBrowseURL_NoFiltersUsesSimpleForm(t *testing.T) {
|
|
u := buildBrowseURL(1, BrowseOptions{})
|
|
if u != "https://www.furaffinity.net/browse/" {
|
|
t.Errorf("page=1 default = %q", u)
|
|
}
|
|
u = buildBrowseURL(2, BrowseOptions{})
|
|
if u != "https://www.furaffinity.net/browse/?page=2" {
|
|
t.Errorf("page=2 default = %q", u)
|
|
}
|
|
}
|
|
|
|
func TestBuildBrowseURL_RatingsAndCategoryFilters(t *testing.T) {
|
|
got := buildBrowseURL(1, BrowseOptions{
|
|
Ratings: []Rating{RatingGeneral, RatingMature},
|
|
Category: 2,
|
|
PerPage: 48,
|
|
})
|
|
q := mustQuery(t, got)
|
|
if q.Get("rating_general") != "1" || q.Get("rating_mature") != "1" {
|
|
t.Errorf("ratings not encoded: %v", q)
|
|
}
|
|
if q.Get("rating_adult") != "" {
|
|
t.Errorf("rating_adult should not be set when not requested")
|
|
}
|
|
if q.Get("cat") != "2" {
|
|
t.Errorf("cat = %q", q.Get("cat"))
|
|
}
|
|
if q.Get("perpage") != "48" {
|
|
t.Errorf("perpage = %q", q.Get("perpage"))
|
|
}
|
|
}
|
|
|
|
func TestE2E_Browse_RatingsFilterFlowsThroughIterator(t *testing.T) {
|
|
var seenRatings []string
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/browse/", func(w http.ResponseWriter, r *http.Request) {
|
|
seenRatings = []string{
|
|
r.URL.Query().Get("rating_general"),
|
|
r.URL.Query().Get("rating_mature"),
|
|
r.URL.Query().Get("rating_adult"),
|
|
}
|
|
w.Header().Set("Content-Type", "text/html")
|
|
_, _ = w.Write([]byte(`<html><body></body></html>`))
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client := newE2EClient(t, srv)
|
|
for _, err := range client.Browse(context.Background(), BrowseOptions{
|
|
Ratings: []Rating{RatingGeneral},
|
|
}) {
|
|
if err != nil {
|
|
t.Fatalf("iter: %v", err)
|
|
}
|
|
}
|
|
if seenRatings[0] != "1" || seenRatings[1] != "" || seenRatings[2] != "" {
|
|
t.Errorf("seenRatings = %v; want [1, '', '']", seenRatings)
|
|
}
|
|
}
|
|
|
|
// TestFindFavLinks_RealFixture confirms findFavLinks scrapes the +Fav anchor
|
|
// out of a real captured /view/ page. Guards against FA changing the fav-link
|
|
// markup (issue #5: favourite action doing nothing).
|
|
func TestFindFavLinks_RealFixture(t *testing.T) {
|
|
raw := loadFixture(t, "submission.html")
|
|
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(raw))
|
|
if err != nil {
|
|
t.Fatalf("read doc: %v", err)
|
|
}
|
|
// submission.html was captured for submission 65052636 (a "+Fav" page).
|
|
favURL, unfavURL := findFavLinks(doc, 65052636)
|
|
if favURL == "" {
|
|
t.Error("findFavLinks: favURL empty +Fav anchor not found in real markup")
|
|
}
|
|
if !strings.Contains(favURL, "/fav/65052636/") || !strings.Contains(favURL, "key=") {
|
|
t.Errorf("findFavLinks: favURL = %q; want a /fav/65052636/?key=... URL", favURL)
|
|
}
|
|
if unfavURL != "" {
|
|
t.Errorf("findFavLinks: unfavURL = %q; want empty on a not-yet-faved page", unfavURL)
|
|
}
|
|
}
|
|
|
|
// TestFindWatchLinks_RealFixture confirms findWatchLinks scrapes the
|
|
// watch-button anchor out of a real captured userpage-style page. Guards
|
|
// against FA changing the watch-button markup (issue #10).
|
|
func TestFindWatchLinks_RealFixture(t *testing.T) {
|
|
raw := loadFixture(t, "gallery_page1.html")
|
|
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(raw))
|
|
if err != nil {
|
|
t.Fatalf("read doc: %v", err)
|
|
}
|
|
// gallery_page1.html was captured for kazucreations, whom the capturing
|
|
// account watches so the header shows an Unwatch button.
|
|
watchURL, unwatchURL := findWatchLinks(doc, "kazucreations")
|
|
if unwatchURL == "" {
|
|
t.Error("findWatchLinks: unwatchURL empty watch-button anchor not found in real markup")
|
|
}
|
|
if unwatchURL != "" && (!strings.Contains(unwatchURL, "/unwatch/kazucreations/") || !strings.Contains(unwatchURL, "key=")) {
|
|
t.Errorf("findWatchLinks: unwatchURL = %q; want a /unwatch/kazucreations/?key=... URL", unwatchURL)
|
|
}
|
|
if watchURL != "" {
|
|
t.Errorf("findWatchLinks: watchURL = %q; want empty when already watching", watchURL)
|
|
}
|
|
}
|