Files
RenewCMS/test/site_pagination_http_test.go
T

257 lines
8.6 KiB
Go

package test
import (
"RenewCMS/api/pkg/apperr"
"RenewCMS/internal/domain/site"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strconv"
"strings"
"testing"
)
func seedPagedSites(e *siteEnv, count int, shops int) {
e.sites.seed(1, "Renew", "renew", true)
for id := uint32(2); id <= uint32(count); id++ {
name := fmt.Sprintf("Site %02d", id)
key := fmt.Sprintf("site%02d", id)
if int(id) <= shops+1 {
name = fmt.Sprintf("Shop %02d", id)
key = fmt.Sprintf("shop%02d", id)
}
e.sites.seed(id, name, key, false)
}
}
func siteNames(sites []site.Site) []string {
names := []string{}
for _, s := range sites {
names = append(names, s.Name)
}
return names
}
func siteIDs(sites []site.Site) []uint32 {
ids := []uint32{}
for _, s := range sites {
ids = append(ids, s.ID)
}
return ids
}
func TestSitesPagination(t *testing.T) {
list := func(t *testing.T, e *siteEnv, query string) (*httptest.ResponseRecorder, []site.Site) {
t.Helper()
rec := e.owner(t, http.MethodGet, "/v1/site/"+query, "")
if rec.Code != http.StatusOK {
return rec, nil
}
return rec, decodeSiteList(t, rec)
}
t.Run("With no paging parameter every site is returned", func(t *testing.T) {
e := newSiteEnv(t)
seedPagedSites(e, 61, 0)
rec, sites := list(t, e, "")
if len(sites) != 61 || sites[0].Name != "Renew" || rec.Header().Get("X-Total-Count") != "61" {
t.Fatalf("got %d sites, first %q, total %q", len(sites), sites[0].Name, rec.Header().Get("X-Total-Count"))
}
})
t.Run("q alone returns every match", func(t *testing.T) {
e := newSiteEnv(t)
seedPagedSites(e, 61, 40)
rec, sites := list(t, e, "?q=shop")
if len(sites) != 40 || rec.Header().Get("X-Total-Count") != "40" {
t.Fatalf("got %d sites, total %q", len(sites), rec.Header().Get("X-Total-Count"))
}
})
t.Run("Paging and sizes", func(t *testing.T) {
cases := []struct {
params string
count int
}{{"page=1", 25}, {"page=2", 25}, {"page=3", 11}, {"size=5", 5}, {"size=25", 25}, {"size=50&page=2", 11}, {"size=100", 61}}
for _, c := range cases {
t.Run(c.params, func(t *testing.T) {
e := newSiteEnv(t)
seedPagedSites(e, 61, 0)
rec, sites := list(t, e, "?"+c.params)
if len(sites) != c.count || rec.Header().Get("X-Total-Count") != "61" {
t.Fatalf("got %d sites, total %q", len(sites), rec.Header().Get("X-Total-Count"))
}
})
}
})
t.Run("Pages never overlap and keep the order", func(t *testing.T) {
e := newSiteEnv(t)
seedPagedSites(e, 61, 0)
_, unpaged := list(t, e, "")
joined := []site.Site{}
for page := 1; page <= 3; page++ {
_, part := list(t, e, "?page="+strconv.Itoa(page))
joined = append(joined, part...)
}
if !reflect.DeepEqual(siteIDs(joined), siteIDs(unpaged)) || joined[0].Name != "Renew" {
t.Fatalf("pages differ from the unpaged list")
}
})
t.Run("A page past the end is an empty array with the total", func(t *testing.T) {
e := newSiteEnv(t)
seedPagedSites(e, 61, 0)
rec, _ := list(t, e, "?page=99")
if strings.TrimSpace(rec.Body.String()) != "[]" || rec.Header().Get("X-Total-Count") != "61" {
t.Fatalf("body %q total %q", rec.Body.String(), rec.Header().Get("X-Total-Count"))
}
})
t.Run("No match is an empty array", func(t *testing.T) {
e := newSiteEnv(t)
seedPagedSites(e, 3, 0)
rec, _ := list(t, e, "?q=nomatch")
if strings.TrimSpace(rec.Body.String()) != "[]" || rec.Header().Get("X-Total-Count") != "0" {
t.Fatalf("body %q total %q", rec.Body.String(), rec.Header().Get("X-Total-Count"))
}
})
t.Run("Search matches name, key and base URL", func(t *testing.T) {
cases := []struct {
q string
names []string
}{
{"renew", []string{"Renew"}},
{"JOURNAL", []string{"Journal"}},
{"blog", []string{"Journal"}},
{"example.org", []string{"Journal"}},
{"example", []string{"Renew", "Archive", "Journal"}},
{"nobody", []string{}},
}
for _, c := range cases {
t.Run(c.q, func(t *testing.T) {
e := newSiteEnv(t)
e.sites.seed(1, "Renew", "renew", true)
journal := e.sites.seed(2, "Journal", "blog", false)
journal.BaseURL = "https://journal.example.org"
e.sites.sites[2] = journal
archive := e.sites.seed(3, "Archive", "archive", false)
archive.BaseURL = "https://old.example.net"
e.sites.sites[3] = archive
rec, sites := list(t, e, "?q="+url.QueryEscape(c.q))
if !reflect.DeepEqual(siteNames(sites), c.names) || rec.Header().Get("X-Total-Count") != strconv.Itoa(len(c.names)) {
t.Fatalf("names %v total %q", siteNames(sites), rec.Header().Get("X-Total-Count"))
}
})
}
})
t.Run("Search ignores accents", func(t *testing.T) {
e := newSiteEnv(t)
e.sites.seed(1, "Café", "cafe", true)
for _, q := range []string{"cafe", "CAFÉ"} {
rec, sites := list(t, e, "?q="+url.QueryEscape(q))
if !reflect.DeepEqual(siteNames(sites), []string{"Café"}) || rec.Header().Get("X-Total-Count") != "1" {
t.Fatalf("%s: names %v", q, siteNames(sites))
}
}
})
t.Run("Search and paging compose and the total follows the search", func(t *testing.T) {
e := newSiteEnv(t)
seedPagedSites(e, 61, 27)
rec, sites := list(t, e, "?q=shop&page=2")
if len(sites) != 2 || rec.Header().Get("X-Total-Count") != "27" {
t.Fatalf("got %d sites, total %q", len(sites), rec.Header().Get("X-Total-Count"))
}
})
t.Run("The default site is not forced into the results", func(t *testing.T) {
e := newSiteEnv(t)
e.sites.seed(1, "Renew", "renew", true)
e.sites.seed(2, "Journal", "blog", false)
_, sites := list(t, e, "?q=journal")
if !reflect.DeepEqual(siteNames(sites), []string{"Journal"}) {
t.Fatalf("names %v", siteNames(sites))
}
})
t.Run("A wildcard in q is literal", func(t *testing.T) {
e := newSiteEnv(t)
e.sites.seed(1, "Main", "main", true)
e.sites.seed(2, "100% Shop", "shop", false)
_, sites := list(t, e, "?q=%25")
if !reflect.DeepEqual(siteNames(sites), []string{"100% Shop"}) {
t.Fatalf("names %v", siteNames(sites))
}
})
t.Run("Invalid page, size or q", func(t *testing.T) {
for _, params := range []string{"page=0", "page=-1", "page=abc", "page=", "size=0", "size=30", "size=101", "size=abc", "size=", "q=" + strings.Repeat("a", 201)} {
t.Run(params[:min(len(params), 12)], func(t *testing.T) {
e := newSiteEnvWithMain(t)
rec := e.owner(t, http.MethodGet, "/v1/site/?"+params, "")
expectSiteStatusAndBody(t, rec, http.StatusBadRequest, apperr.BodyErrorMessage)
if _, present := rec.Header()["X-Total-Count"]; present {
t.Errorf("X-Total-Count sent on a refusal")
}
})
}
})
t.Run("q at the limit is accepted", func(t *testing.T) {
e := newSiteEnvWithMain(t)
rec, _ := list(t, e, "?q="+strings.Repeat("a", 200))
if rec.Code != http.StatusOK || strings.TrimSpace(rec.Body.String()) != "[]" || rec.Header().Get("X-Total-Count") != "0" {
t.Fatalf("status %d body %q", rec.Code, rec.Body.String())
}
})
t.Run("The other fields are unchanged", func(t *testing.T) {
e := newSiteEnvWithMain(t)
e.seedArticle(1, siteMainID, true)
rec, sites := list(t, e, "?size=5")
if len(sites) != 1 || sites[0].ArticleCount != 1 || !strings.Contains(rec.Body.String(), `"article_count"`) {
t.Fatalf("body %s", rec.Body.String())
}
})
t.Run("Readers and refusals", func(t *testing.T) {
cases := []struct {
credentials string
send func(e *siteEnv, t *testing.T) *httptest.ResponseRecorder
status int
}{
{"the owner's jwt cookie", func(e *siteEnv, t *testing.T) *httptest.ResponseRecorder {
return e.as(t, apiKeyOwnerId, http.MethodGet, "/v1/site/?size=5", "")
}, http.StatusOK},
{"a verified admin's jwt cookie", func(e *siteEnv, t *testing.T) *httptest.ResponseRecorder {
return e.as(t, apiKeyDevId, http.MethodGet, "/v1/site/?size=5", "")
}, http.StatusOK},
{"a valid API key", func(e *siteEnv, t *testing.T) *httptest.ResponseRecorder {
raw, _ := e.issueKey(apiKeyDevId, "Deploy bot")
return e.asKey(t, raw, http.MethodGet, "/v1/site/?size=5", "")
}, http.StatusOK},
{"an unverified account's jwt cookie", func(e *siteEnv, t *testing.T) *httptest.ResponseRecorder {
return e.as(t, apiKeyQuinnId, http.MethodGet, "/v1/site/?size=5", "")
}, http.StatusForbidden},
{"no credentials", func(e *siteEnv, t *testing.T) *httptest.ResponseRecorder {
return e.anonymous(t, http.MethodGet, "/v1/site/?size=5", "")
}, http.StatusUnauthorized},
}
for _, c := range cases {
t.Run(c.credentials, func(t *testing.T) {
e := newSiteEnvWithMain(t)
rec := c.send(e, t)
_, present := rec.Header()["X-Total-Count"]
if rec.Code != c.status || present != (c.status == http.StatusOK) {
t.Errorf("status %d, header present %v", rec.Code, present)
}
})
}
})
}