232 lines
8.3 KiB
Go
232 lines
8.3 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/site"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const journalPattern = "https://journal.example.com/{lang}/{slug}"
|
|
|
|
func patternBody(value string) string {
|
|
encoded, _ := json.Marshal(value)
|
|
return `{"public_url_pattern":` + string(encoded) + `}`
|
|
}
|
|
|
|
func newPatternEnv(t *testing.T) *localizationEnv {
|
|
t.Helper()
|
|
e := newLocalizationEnv(t)
|
|
e.sites.seed(siteMainID, "Main", "main", true)
|
|
return e
|
|
}
|
|
|
|
func TestSitePublicURLPattern(t *testing.T) {
|
|
t.Run("Reading the pattern", func(t *testing.T) {
|
|
e := newPatternEnv(t)
|
|
stored := e.sites.sites[siteMainID]
|
|
pattern := journalPattern
|
|
stored.PublicURLPattern = &pattern
|
|
e.sites.sites[siteMainID] = stored
|
|
|
|
rec := e.as(t, readerUserID, http.MethodGet, "/v1/site/", "")
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
|
}
|
|
var items []map[string]json.RawMessage
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(items[0]["public_url_pattern"]) != `"`+journalPattern+`"` {
|
|
t.Errorf("pattern = %s", items[0]["public_url_pattern"])
|
|
}
|
|
for _, key := range []string{"id", "name", "key", "base_url", "default_social_image_id", "default_robots", "locale", "is_default", "article_count", "site_name", "title_template", "default_description"} {
|
|
if _, present := items[0][key]; !present {
|
|
t.Errorf("key %q missing", key)
|
|
}
|
|
}
|
|
})
|
|
|
|
writes := []struct {
|
|
name string
|
|
raw string
|
|
want *string
|
|
start string
|
|
}{
|
|
{"a pattern with lang and slug", patternBody(journalPattern), localeOf(journalPattern), ""},
|
|
{"a pattern with a path", patternBody("https://journal.example.com/{lang}/articles/{slug}"), localeOf("https://journal.example.com/{lang}/articles/{slug}"), ""},
|
|
{"a local http pattern", patternBody("http://localhost:3000/{slug}"), localeOf("http://localhost:3000/{slug}"), ""},
|
|
{"a repeated placeholder", patternBody("https://journal.example.com/{slug}/{slug}"), localeOf("https://journal.example.com/{slug}/{slug}"), ""},
|
|
{"null", `{"public_url_pattern":null}`, nil, journalPattern},
|
|
{"an empty string", patternBody(""), nil, journalPattern},
|
|
{"a padded pattern is trimmed", patternBody(" https://journal.example.com/{slug} "), localeOf("https://journal.example.com/{slug}"), ""},
|
|
}
|
|
for _, c := range writes {
|
|
t.Run("Writing the pattern/"+c.name, func(t *testing.T) {
|
|
e := newPatternEnv(t)
|
|
if c.start != "" {
|
|
stored := e.sites.sites[siteMainID]
|
|
stored.PublicURLPattern = localeOf(c.start)
|
|
e.sites.sites[siteMainID] = stored
|
|
}
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/site/1", c.raw)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body)
|
|
}
|
|
if got := decodeSiteBody(t, rec).PublicURLPattern; !sameText(got, c.want) {
|
|
t.Errorf("returned = %v", got)
|
|
}
|
|
if !sameText(e.sites.sites[siteMainID].PublicURLPattern, c.want) {
|
|
t.Errorf("stored = %v", e.sites.sites[siteMainID].PublicURLPattern)
|
|
}
|
|
})
|
|
}
|
|
|
|
refusals := []struct {
|
|
value string
|
|
code string
|
|
}{
|
|
{"ftp://journal.example.com/{lang}/{slug}", "invalid_scheme"},
|
|
{"journal.example.com/{slug}", "invalid_scheme"},
|
|
{"https:///{slug}", "invalid_host"},
|
|
{"https://journal.example.com/{language}/{slug}", "unknown_placeholder"},
|
|
{"https://journal.example.com/{lang", "unknown_placeholder"},
|
|
{"https://journal.example.com/my pages/{lang}/{slug}", "not_encoded"},
|
|
{"https://journal.example.com/é/{slug}", "not_encoded"},
|
|
{"https://journal.example.com/{lang}", "missing_slug"},
|
|
{"https://example.com/" + strings.Repeat("a", 494) + "{slug}", "invalid_pattern"},
|
|
{"ftp://x.example.com/{language} here", "invalid_scheme"},
|
|
{"https://x.example.com/{language} here", "unknown_placeholder"},
|
|
{"https://x.example.com/here there", "not_encoded"},
|
|
}
|
|
for _, c := range refusals {
|
|
t.Run("Patterns that are refused/"+c.code+" "+truncate(c.value), func(t *testing.T) {
|
|
e := newPatternEnv(t)
|
|
stored := e.sites.sites[siteMainID]
|
|
stored.PublicURLPattern = localeOf(journalPattern)
|
|
e.sites.sites[siteMainID] = stored
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/site/1", patternBody(c.value))
|
|
|
|
expectSiteStatusAndBody(t, rec, http.StatusUnprocessableEntity, c.code)
|
|
if !sameText(e.sites.sites[siteMainID].PublicURLPattern, localeOf(journalPattern)) {
|
|
t.Errorf("the stored pattern changed")
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("A pattern of 513 characters", func(t *testing.T) {
|
|
long := "https://example.com/" + strings.Repeat("a", 513-len("https://example.com/")-len("{slug}")) + "{slug}"
|
|
if len(long) != 513 {
|
|
t.Fatalf("fixture length %d", len(long))
|
|
}
|
|
e := newPatternEnv(t)
|
|
expectSiteStatusAndBody(t, e.owner(t, http.MethodPatch, "/v1/site/1", patternBody(long)), http.StatusUnprocessableEntity, "invalid_pattern")
|
|
})
|
|
|
|
t.Run("A pattern of 512 characters is accepted", func(t *testing.T) {
|
|
exact := "https://example.com/" + strings.Repeat("a", 512-len("https://example.com/")-len("{slug}")) + "{slug}"
|
|
e := newPatternEnv(t)
|
|
if rec := e.owner(t, http.MethodPatch, "/v1/site/1", patternBody(exact)); rec.Code != http.StatusOK {
|
|
t.Errorf("status %d: %s", rec.Code, rec.Body)
|
|
}
|
|
})
|
|
|
|
t.Run("Only an owner can write", func(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
user uint32
|
|
status int
|
|
}{
|
|
{"an anonymous client", 0, http.StatusUnauthorized},
|
|
{"a reader", readerUserID, http.StatusForbidden},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
e := newPatternEnv(t)
|
|
rec := e.as(t, c.user, http.MethodPatch, "/v1/site/1", patternBody(journalPattern))
|
|
if rec.Code != c.status || e.sites.sites[siteMainID].PublicURLPattern != nil {
|
|
t.Errorf("status %d, stored %v", rec.Code, e.sites.sites[siteMainID].PublicURLPattern)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Saving the pattern changes nothing else", func(t *testing.T) {
|
|
e := newPatternEnv(t)
|
|
before := e.sites.sites[siteMainID]
|
|
|
|
e.owner(t, http.MethodPatch, "/v1/site/1", patternBody(journalPattern))
|
|
|
|
after := e.sites.sites[siteMainID]
|
|
after.PublicURLPattern = nil
|
|
if after != before {
|
|
t.Errorf("site changed: %+v vs %+v", after, before)
|
|
}
|
|
})
|
|
|
|
t.Run("Existing site behaviour is unchanged", func(t *testing.T) {
|
|
e := newPatternEnv(t)
|
|
stored := e.sites.sites[siteMainID]
|
|
stored.PublicURLPattern = localeOf(journalPattern)
|
|
e.sites.sites[siteMainID] = stored
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/site/1", `{"name":"Renamed"}`)
|
|
|
|
if rec.Code != http.StatusOK || !sameText(e.sites.sites[siteMainID].PublicURLPattern, localeOf(journalPattern)) {
|
|
t.Errorf("status %d, pattern %v", rec.Code, e.sites.sites[siteMainID].PublicURLPattern)
|
|
}
|
|
})
|
|
}
|
|
|
|
func truncate(value string) string {
|
|
if len(value) > 40 {
|
|
return value[:40]
|
|
}
|
|
return value
|
|
}
|
|
|
|
func TestPublicURLPatternNormalization(t *testing.T) {
|
|
t.Run("a nil pattern stays unset", func(t *testing.T) {
|
|
if normalized, err := site.NormalizePublicURLPattern(nil); normalized != nil || err != nil {
|
|
t.Errorf("got %v, %v", normalized, err)
|
|
}
|
|
})
|
|
t.Run("a pattern without a host is refused as a host error", func(t *testing.T) {
|
|
if _, err := site.NormalizePublicURLPattern(localeOf("https:///{slug}")); err != site.ErrPublicURLHost {
|
|
t.Errorf("err = %v", err)
|
|
}
|
|
})
|
|
t.Run("percent encoded sequences are accepted and a stray percent is not", func(t *testing.T) {
|
|
if _, err := site.NormalizePublicURLPattern(localeOf("https://a.example.com/%C3%A9/{slug}")); err != nil {
|
|
t.Errorf("encoded: %v", err)
|
|
}
|
|
if _, err := site.NormalizePublicURLPattern(localeOf("https://a.example.com/100%/{slug}")); err != site.ErrPublicURLNotEncoded {
|
|
t.Errorf("stray: %v", err)
|
|
}
|
|
})
|
|
t.Run("a pattern ending in a percent sequence is judged without panicking", func(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
pattern string
|
|
want error
|
|
}{
|
|
{"a lone percent at the end", "https://a.example.com/{slug}%", site.ErrPublicURLNotEncoded},
|
|
{"a percent and one digit at the end", "https://a.example.com/{slug}%4", site.ErrPublicURLNotEncoded},
|
|
{"a percent and a non hex digit at the end", "https://a.example.com/{slug}%4G", site.ErrPublicURLNotEncoded},
|
|
{"a valid sequence at the end", "https://a.example.com/{slug}%41", nil},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if _, err := site.NormalizePublicURLPattern(localeOf(c.pattern)); err != c.want {
|
|
t.Errorf("err = %v, want %v", err, c.want)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|