feat: site languages, setup and site URL settings

This commit is contained in:
Florian Sylvain committed 2026-11-01 22:13:02 +01:00
1 parent 0806733d5c
commit d9158025d4
106 files changed
+10184 -56

No files matched your search

+231
View File
@@ -0,0 +1,231 @@
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)
}
})
}
})
}