714 lines
29 KiB
Go
714 lines
29 KiB
Go
package test
|
|
|
|
import (
|
|
entity "RenewCMS/internal/infrastructure/persistence/models"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
validator "RenewCMS/api/pkg/validator"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const failedToRetrieveArticles = "Failed to retrieve articles\n"
|
|
|
|
func expectIDs(t *testing.T, label string, got []int, want ...int) {
|
|
t.Helper()
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("%s: got %v, want %v", label, got, want)
|
|
}
|
|
}
|
|
|
|
func expectCounts(t *testing.T, got map[string]int, want map[string]int) {
|
|
t.Helper()
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("counts: got %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func bylineNames(item map[string]any, field string) []string {
|
|
names := []string{}
|
|
for _, entry := range item[field].([]any) {
|
|
names = append(names, entry.(map[string]any)["name"].(string))
|
|
}
|
|
return names
|
|
}
|
|
|
|
func TestArticleListLocalizationSQLite(t *testing.T) {
|
|
runArticleListLocalization(t, openContractSQLite)
|
|
}
|
|
|
|
func TestMySQLArticleListLocalization(t *testing.T) {
|
|
runArticleListLocalization(t, openContractMySQL)
|
|
}
|
|
|
|
func TestPostgresArticleListLocalization(t *testing.T) {
|
|
runArticleListLocalization(t, openContractPostgres)
|
|
}
|
|
|
|
func runArticleListLocalization(t *testing.T, open func(*testing.T) *gorm.DB) {
|
|
validator.InitValidator()
|
|
fresh := func(t *testing.T) *localizationListEnv { return newLocalizationListEnv(t, open) }
|
|
shared := fresh(t)
|
|
base := func(t *testing.T) *localizationListEnv {
|
|
shared.t = t
|
|
shared.repo.calls = 0
|
|
shared.repo.failMethod = ""
|
|
return shared
|
|
}
|
|
|
|
t.Run("A bare request is still the flat envelope", func(t *testing.T) {
|
|
env := base(t)
|
|
rec, _ := env.get(localizationOwnerID, "")
|
|
var keys map[string]json.RawMessage
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &keys); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(keys) != 5 || keys["items"] == nil || keys["total"] == nil || keys["page"] == nil || keys["size"] == nil || keys["counts"] == nil {
|
|
t.Fatalf("unexpected envelope keys: %s", rec.Body.String())
|
|
}
|
|
_, body := env.get(localizationOwnerID, "?size=100")
|
|
if body.Total != 17 || len(body.Items) != 17 {
|
|
t.Fatalf("expected 17 versions, got total %d and %d items", body.Total, len(body.Items))
|
|
}
|
|
if _, grouped := body.Items[0]["coverage"]; grouped {
|
|
t.Fatal("the flat item must not carry grouped fields")
|
|
}
|
|
})
|
|
|
|
t.Run("lang, tab and group are not read unless asked", func(t *testing.T) {
|
|
env := base(t)
|
|
_, body := env.get(0, "?size=5")
|
|
if body.Total != 12 || len(body.Items) != 5 {
|
|
t.Fatalf("expected 12 online versions, got total %d and %d items", body.Total, len(body.Items))
|
|
}
|
|
for _, item := range body.Items {
|
|
if _, ok := item["lang"]; !ok {
|
|
t.Fatal("items keep lang")
|
|
}
|
|
if _, ok := item["story_id"]; !ok {
|
|
t.Fatal("items keep story_id")
|
|
}
|
|
}
|
|
expectCounts(t, body.Counts, map[string]int{"all": 12, "online": 12, "draft": 0, "suggestions": 0})
|
|
})
|
|
|
|
t.Run("group=story needs an editorial caller", func(t *testing.T) {
|
|
for name, caller := range map[string]uint32{"no cookie": 0, "malformed cookie": localizationMalformed, "unverified user": localizationUnverifiedID} {
|
|
env := base(t)
|
|
rec, _ := env.get(caller, "?group=story")
|
|
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), paginationBodyMismatch) {
|
|
t.Fatalf("%s: expected 400, got %d %q", name, rec.Code, rec.Body.String())
|
|
}
|
|
if env.repo.calls != 0 {
|
|
t.Fatalf("%s: the repository was called", name)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid new parameters answer 400", func(t *testing.T) {
|
|
queries := []string{
|
|
"group=stories", "group=", "group=story&tab=review", "group=story&tab=published", "group=story&tab=", "tab=missing",
|
|
"group=story&lang=english", "group=story&lang=xx", "group=story&lang=", "group=story&author=0", "group=story&author=abc",
|
|
"group=story&author=", "author=-1", "lang=english", "lang=", "author=0",
|
|
}
|
|
for _, query := range queries {
|
|
env := base(t)
|
|
rec, _ := env.get(localizationOwnerID, "?"+query)
|
|
if rec.Code != http.StatusBadRequest || !strings.Contains(rec.Body.String(), paginationBodyMismatch) {
|
|
t.Fatalf("%s: expected 400, got %d %q", query, rec.Code, rec.Body.String())
|
|
}
|
|
if env.repo.calls != 0 {
|
|
t.Fatalf("%s: the repository was called", query)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("All languages, page 1", func(t *testing.T) {
|
|
env := base(t)
|
|
rec, body := env.get(localizationOwnerID, "?group=story&size=5")
|
|
if rec.Code != http.StatusOK || body.Total != 8 || body.Page != 1 || body.Size != 5 {
|
|
t.Fatalf("unexpected page: %d total=%d page=%d size=%d", rec.Code, body.Total, body.Page, body.Size)
|
|
}
|
|
expectIDs(t, "stories", body.storyIDs(), 1, 2, 3, 4, 5)
|
|
if body.Counts["all"] != 8 {
|
|
t.Fatalf("counts.all = %d", body.Counts["all"])
|
|
}
|
|
})
|
|
|
|
t.Run("All languages, page 2 is the last", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&size=5&page=2")
|
|
expectIDs(t, "stories", body.storyIDs(), 6, 7, 8)
|
|
if body.Total != 8 {
|
|
t.Fatalf("total = %d", body.Total)
|
|
}
|
|
})
|
|
|
|
t.Run("A page beyond the last is empty with the real total", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&size=5&page=99")
|
|
if len(body.Items) != 0 || body.Total != 8 || body.Counts["all"] != 8 {
|
|
t.Fatalf("unexpected page: %d items, total %d, counts %v", len(body.Items), body.Total, body.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("A grouped row describes the representative version and the story", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story")
|
|
item := body.item(1)
|
|
if int(item["id"].(float64)) != 101 || item["lang"] != "en" || item["site_id"] == nil || item["slug"] != "a-101" || item["is_online"] != true {
|
|
t.Fatalf("unexpected representative: %v", item)
|
|
}
|
|
if item["story_updated_at"] != "2026-10-30T10:00:00Z" {
|
|
t.Fatalf("story_updated_at = %v", item["story_updated_at"])
|
|
}
|
|
if names := bylineNames(item, "story_authors"); !reflect.DeepEqual(names, []string{"Marta Alves"}) {
|
|
t.Fatalf("story_authors = %v", names)
|
|
}
|
|
for _, field := range []string{"coverage", "missing_langs", "capabilities"} {
|
|
if item[field] == nil {
|
|
t.Fatalf("%s is missing", field)
|
|
}
|
|
}
|
|
entry := item["coverage"].([]any)[0].(map[string]any)
|
|
for _, field := range []string{"id", "lang", "title", "is_online", "language_enabled", "capabilities"} {
|
|
if _, ok := entry[field]; !ok {
|
|
t.Fatalf("coverage entry lacks %s", field)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("The representative is the default-language version, else the first readable by language code", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story")
|
|
for story, want := range map[int]int{1: 101, 4: 401, 8: 801} {
|
|
if got := int(body.item(story)["id"].(float64)); got != want {
|
|
t.Fatalf("story %d representative = %d, want %d", story, got, want)
|
|
}
|
|
}
|
|
if body.item(8)["lang"] != "es" {
|
|
t.Fatalf("story 8 lang = %v", body.item(8)["lang"])
|
|
}
|
|
})
|
|
|
|
t.Run("The representative falls back when the default version is unreadable", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.must(env.db.Model(&entity.Article{}).Where("id = ?", 502).UpdateColumn("is_online", true).Error)
|
|
env.must(env.db.Model(&entity.Article{}).Where("id = ?", 501).UpdateColumn("is_online", false).Error)
|
|
_, body := env.get(localizationWriterID, "?group=story")
|
|
item := body.item(5)
|
|
if int(item["id"].(float64)) != 502 || item["lang"] != "de" {
|
|
t.Fatalf("expected fallback to 502, got %v", item["id"])
|
|
}
|
|
if len(item["coverage"].([]any)) != 1 {
|
|
t.Fatalf("the hidden version leaks: %v", item["coverage"])
|
|
}
|
|
})
|
|
|
|
t.Run("story_updated_at is the latest readable version update", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.must(env.db.Model(&entity.Article{}).Where("id = ?", 102).UpdateColumn("updated_at", time.Date(2026, 11, 1, 8, 0, 0, 0, time.UTC)).Error)
|
|
_, writer := env.get(localizationWriterID, "?group=story")
|
|
if got := writer.item(1)["story_updated_at"]; got != "2026-10-30T09:00:00Z" {
|
|
t.Fatalf("writer story_updated_at = %v", got)
|
|
}
|
|
_, owner := env.get(localizationOwnerID, "?group=story")
|
|
if got := owner.item(1)["story_updated_at"]; got != "2026-11-01T08:00:00Z" {
|
|
t.Fatalf("owner story_updated_at = %v", got)
|
|
}
|
|
expectIDs(t, "owner order", owner.storyIDs()[:2], 1, 2)
|
|
})
|
|
|
|
t.Run("Coverage lists readable versions in the chip order, then the languages that can be created", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story")
|
|
cases := []struct {
|
|
story int
|
|
coverage []string
|
|
missing []string
|
|
}{
|
|
{1, []string{"en", "ar", "de", "fr"}, []string{}},
|
|
{3, []string{"en", "fr"}, []string{"ar", "de"}},
|
|
{4, []string{"en"}, []string{"ar", "de", "fr"}},
|
|
{8, []string{"es"}, []string{"en", "ar", "de", "fr"}},
|
|
}
|
|
for _, c := range cases {
|
|
item := body.item(c.story)
|
|
if got := coverageLangs(item); !reflect.DeepEqual(got, c.coverage) {
|
|
t.Fatalf("story %d coverage = %v", c.story, got)
|
|
}
|
|
if got := stringsOf(item["missing_langs"]); !reflect.DeepEqual(got, c.missing) {
|
|
t.Fatalf("story %d missing_langs = %v", c.story, got)
|
|
}
|
|
}
|
|
if body.item(8)["coverage"].([]any)[0].(map[string]any)["language_enabled"] != false {
|
|
t.Fatal("es is disabled")
|
|
}
|
|
})
|
|
|
|
t.Run("Coverage order is default, other enabled by tag, disabled by tag; missing is listed by the same rule", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.addVersion(localizationVersion{id: 802, lang: "it", online: true, title: "Journal 802", updated: localizationDay(10, 8), byline: "Luis Ferrer", noindex: true, storyKey: 8})
|
|
_, body := env.get(localizationOwnerID, "?group=story")
|
|
item := body.item(8)
|
|
if got := coverageLangs(item); !reflect.DeepEqual(got, []string{"es", "it"}) {
|
|
t.Fatalf("coverage = %v", got)
|
|
}
|
|
if got := stringsOf(item["missing_langs"]); got[0] != "en" {
|
|
t.Fatalf("missing_langs = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("A caller who cannot create gets no missing_langs", func(t *testing.T) {
|
|
_, body := base(t).get(localizationReaderID, "?group=story")
|
|
for _, item := range body.Items {
|
|
if len(stringsOf(item["missing_langs"])) != 0 {
|
|
t.Fatalf("reader got missing_langs on %v", item["story_id"])
|
|
}
|
|
}
|
|
if got := coverageLangs(body.item(1)); !reflect.DeepEqual(got, []string{"en", "de"}) {
|
|
t.Fatalf("reader coverage = %v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("The row of a story whose versions are all hidden does not exist", func(t *testing.T) {
|
|
_, body := base(t).get(localizationWriterID, "?group=story&size=100")
|
|
if body.item(4) != nil || body.Total != 7 || body.Counts["all"] != 7 {
|
|
t.Fatalf("story 4 leaks: total=%d counts=%v", body.Total, body.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("Hidden versions leave no trace", func(t *testing.T) {
|
|
env := fresh(t)
|
|
_, with := env.get(localizationWriterID, "?group=story&size=100")
|
|
withRec, _ := env.get(localizationWriterID, "?group=story&size=100")
|
|
without := fresh(t)
|
|
without.must(without.db.Unscoped().Where("id IN ?", []uint32{102, 104, 401, 502, 602}).Delete(&entity.Article{}).Error)
|
|
withoutRec, _ := without.get(localizationWriterID, "?group=story&size=100")
|
|
if withRec.Body.String() != withoutRec.Body.String() {
|
|
t.Fatalf("hidden versions change the response:\n%s\n%s", withRec.Body.String(), withoutRec.Body.String())
|
|
}
|
|
if len(with.Items) == 0 {
|
|
t.Fatal("expected rows")
|
|
}
|
|
})
|
|
|
|
t.Run("Unassigned articles are their own story with an empty language", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.addVersion(localizationVersion{id: 30, lang: "", online: true, title: "Journal 30", updated: localizationDay(1, 9), byline: "Luis Ferrer", noindex: true, storyKey: 30})
|
|
_, body := env.get(localizationOwnerID, "?group=story&size=100")
|
|
item := body.item(30)
|
|
if item["lang"] != "" || len(item["coverage"].([]any)) != 1 || len(stringsOf(item["missing_langs"])) != 0 {
|
|
t.Fatalf("unexpected unassigned row: %v", item)
|
|
}
|
|
_, missing := env.get(localizationOwnerID, "?group=story&lang=fr&tab=missing&size=100")
|
|
if missing.item(30) != nil {
|
|
t.Fatal("unassigned article listed in the not-in view")
|
|
}
|
|
_, needs := env.get(localizationOwnerID, "?group=story&tab=missing&size=100")
|
|
if needs.item(30) != nil {
|
|
t.Fatal("unassigned article listed as needing translation")
|
|
}
|
|
})
|
|
|
|
t.Run("French selected", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&lang=fr&size=5")
|
|
if body.Total != 4 {
|
|
t.Fatalf("total = %d", body.Total)
|
|
}
|
|
expectIDs(t, "stories", body.storyIDs(), 1, 2, 3, 6)
|
|
expectIDs(t, "representatives", body.articleIDs(), 102, 202, 302, 602)
|
|
if len(body.item(1)["coverage"].([]any)) != 4 || len(bylineNames(body.item(1), "story_authors")) != 1 {
|
|
t.Fatalf("the row must carry the whole story: %v", body.item(1))
|
|
}
|
|
})
|
|
|
|
t.Run("Disabled languages can be selected", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&lang=es")
|
|
expectIDs(t, "articles", body.articleIDs(), 801)
|
|
if body.Total != 1 || body.item(8)["coverage"].([]any)[0].(map[string]any)["language_enabled"] != false {
|
|
t.Fatalf("unexpected result: %v", body)
|
|
}
|
|
})
|
|
|
|
t.Run("A language nobody has gives an empty list", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&lang=it")
|
|
if len(body.Items) != 0 || body.Total != 0 {
|
|
t.Fatalf("expected nothing, got %d items", len(body.Items))
|
|
}
|
|
})
|
|
|
|
t.Run("lang also narrows the flat list", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?lang=fr")
|
|
if body.Total != 4 || len(body.Items) != 4 {
|
|
t.Fatalf("total = %d", body.Total)
|
|
}
|
|
for _, item := range body.Items {
|
|
if item["lang"] != "fr" {
|
|
t.Fatalf("item with lang %v", item["lang"])
|
|
}
|
|
}
|
|
if body.Counts["all"] != 4 || body.Counts["online"] != 2 || body.Counts["draft"] != 2 {
|
|
t.Fatalf("counts = %v", body.Counts)
|
|
}
|
|
_, filtered := base(t).get(localizationOwnerID, "?lang=fr&status=draft&q=zinc")
|
|
if filtered.Counts["all"] != 4 {
|
|
t.Fatalf("flat counts must ignore q and status: %v", filtered.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("Not in French", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&lang=fr&tab=missing&size=5")
|
|
if body.Total != 4 {
|
|
t.Fatalf("total = %d", body.Total)
|
|
}
|
|
expectIDs(t, "stories", body.storyIDs(), 4, 5, 7, 8)
|
|
expectIDs(t, "representatives", body.articleIDs(), 401, 501, 701, 801)
|
|
for _, item := range body.Items {
|
|
if !listHasTag(stringsOf(item["missing_langs"]), "fr") {
|
|
t.Fatalf("missing_langs lacks fr: %v", item["missing_langs"])
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Needs translation, no language selected", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&tab=missing&size=5")
|
|
if body.Total != 7 {
|
|
t.Fatalf("total = %d", body.Total)
|
|
}
|
|
expectIDs(t, "stories", body.storyIDs(), 2, 3, 4, 5, 6)
|
|
})
|
|
|
|
t.Run("Missing means no readable version, not no stored version", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.must(env.db.Model(&entity.Article{}).Where("id = ?", 401).UpdateColumn("is_online", true).Error)
|
|
env.addVersion(localizationVersion{id: 402, lang: "fr", online: false, title: "Hidden French draft", updated: localizationDay(27, 8), byline: "Marta Alves", noindex: true, storyKey: 4})
|
|
rec, body := env.get(localizationWriterID, "?group=story&lang=fr&tab=missing&size=100")
|
|
if body.item(4) == nil {
|
|
t.Fatal("story 4 must be listed as not in French")
|
|
}
|
|
if strings.Contains(rec.Body.String(), "402") || strings.Contains(rec.Body.String(), "Hidden French draft") {
|
|
t.Fatal("the response tells a fr version exists")
|
|
}
|
|
})
|
|
|
|
t.Run("Sites that do not enable the language are excluded", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.addVersion(localizationVersion{id: 5001, lang: "en", online: true, title: "Atelier one", updated: localizationDay(20, 9), byline: "Luis Ferrer", noindex: true, storyKey: 50, siteKey: "atelier"})
|
|
env.addVersion(localizationVersion{id: 5002, lang: "fr", online: true, title: "Atelier un", updated: localizationDay(20, 8), byline: "Luis Ferrer", noindex: true, storyKey: 50, siteKey: "atelier"})
|
|
_, german := env.get(localizationOwnerID, "?group=story&lang=de&tab=missing&size=100")
|
|
if german.item(50) != nil || german.Total == 0 {
|
|
t.Fatalf("atelier story listed or journal empty: %v", german.storyIDs())
|
|
}
|
|
_, spanish := env.get(localizationOwnerID, "?group=story&lang=es&tab=missing")
|
|
if spanish.Total != 0 || len(spanish.Items) != 0 {
|
|
t.Fatalf("es is disabled, got %d", spanish.Total)
|
|
}
|
|
})
|
|
|
|
t.Run("Needs translation counts only languages the caller can create", func(t *testing.T) {
|
|
env := base(t)
|
|
_, body := env.get(localizationReaderID, "?group=story&tab=missing")
|
|
if body.Total != 0 || len(body.Items) != 0 {
|
|
t.Fatalf("expected nothing, got %d", body.Total)
|
|
}
|
|
_, french := env.get(localizationReaderID, "?group=story&lang=fr&tab=missing&size=100")
|
|
expectIDs(t, "reader stories", french.storyIDs(), 1, 5, 6, 7, 8)
|
|
})
|
|
|
|
t.Run("Nothing missing", func(t *testing.T) {
|
|
env := fresh(t)
|
|
for story := uint32(60); story < 63; story++ {
|
|
env.addVersion(localizationVersion{id: story * 100, lang: "en", online: true, title: "Press en", updated: localizationDay(5, 9), byline: "Luis Ferrer", noindex: true, storyKey: story, siteKey: "press"})
|
|
env.addVersion(localizationVersion{id: story*100 + 1, lang: "fr", online: true, title: "Press fr", updated: localizationDay(5, 8), byline: "Luis Ferrer", noindex: true, storyKey: story, siteKey: "press"})
|
|
}
|
|
_, body := env.get(localizationOwnerID, "?group=story&tab=missing&site=press")
|
|
if len(body.Items) != 0 || body.Total != 0 || body.Counts["missing"] != 0 || body.Counts["all"] != 3 {
|
|
t.Fatalf("unexpected: total=%d counts=%v", body.Total, body.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("Status matches a readable version of the story", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&status=draft")
|
|
expectIDs(t, "stories", body.storyIDs(), 1, 4, 5, 6)
|
|
if body.Total != 4 {
|
|
t.Fatalf("total = %d", body.Total)
|
|
}
|
|
})
|
|
|
|
t.Run("Combined filters need a single readable version to satisfy them together", func(t *testing.T) {
|
|
env := fresh(t)
|
|
camille := env.authors["Camille Roche"]
|
|
env.must(env.db.Model(&entity.ArticleAuthor{}).Where("article_id IN ?", []uint32{102, 104}).UpdateColumn("author_id", camille).Error)
|
|
marta := env.authors["Marta Alves"]
|
|
_, draftMarta := env.get(localizationOwnerID, "?group=story&status=draft&author="+itoa(marta))
|
|
if draftMarta.item(1) != nil {
|
|
t.Fatal("story 1 must not be listed")
|
|
}
|
|
_, onlineMarta := env.get(localizationOwnerID, "?group=story&status=online&author="+itoa(marta))
|
|
if onlineMarta.item(1) == nil {
|
|
t.Fatal("story 1 must be listed")
|
|
}
|
|
_, draftCamille := env.get(localizationOwnerID, "?group=story&status=draft&author="+itoa(camille))
|
|
if draftCamille.item(1) == nil {
|
|
t.Fatal("story 1 must be listed")
|
|
}
|
|
_, union := env.get(localizationOwnerID, "?group=story&author="+itoa(camille))
|
|
if names := bylineNames(union.item(1), "story_authors"); !reflect.DeepEqual(names, []string{"Marta Alves", "Camille Roche"}) {
|
|
t.Fatalf("story_authors = %v", names)
|
|
}
|
|
})
|
|
|
|
t.Run("Search matches any readable title of the story", func(t *testing.T) {
|
|
env := base(t)
|
|
_, body := env.get(localizationOwnerID, "?group=story&q=RENOVATION")
|
|
expectIDs(t, "articles", body.articleIDs(), 101)
|
|
_, french := env.get(localizationOwnerID, "?group=story&lang=fr&q=chiffrons")
|
|
expectIDs(t, "french", french.articleIDs(), 102)
|
|
_, english := env.get(localizationOwnerID, "?group=story&lang=en&q=chiffrons")
|
|
if len(english.Items) != 0 {
|
|
t.Fatal("expected nothing")
|
|
}
|
|
_, accent := env.get(localizationOwnerID, "?group=story&q=r%C3%A9novation")
|
|
expectIDs(t, "accent", accent.articleIDs(), 101)
|
|
})
|
|
|
|
t.Run("Search in the not-in view matches readable source titles", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&lang=fr&tab=missing&q=Spring")
|
|
expectIDs(t, "stories", body.storyIDs(), 4)
|
|
})
|
|
|
|
t.Run("Author in language mode", func(t *testing.T) {
|
|
env := base(t)
|
|
_, body := env.get(localizationOwnerID, "?group=story&lang=fr&author="+itoa(env.authors["Camille Roche"]))
|
|
expectIDs(t, "stories", body.storyIDs(), 2, 6)
|
|
})
|
|
|
|
t.Run("Site filter", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.addVersion(localizationVersion{id: 7001, lang: "en", online: true, title: "Press piece", updated: localizationDay(6, 9), byline: "Luis Ferrer", noindex: true, storyKey: 70, siteKey: "press"})
|
|
_, body := env.get(localizationOwnerID, "?group=story&site=press")
|
|
expectIDs(t, "stories", body.storyIDs(), 70)
|
|
_, unknown := env.get(localizationOwnerID, "?group=story&site=nowhere")
|
|
if len(unknown.Items) != 0 || unknown.Total != 0 {
|
|
t.Fatal("unknown site must be empty")
|
|
}
|
|
})
|
|
|
|
t.Run("SEO filter on one version", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.must(env.db.Model(&entity.Article{}).Where("id = ?", 502).UpdateColumn("noindex", false).Error)
|
|
_, body := env.get(localizationOwnerID, "?group=story&seo=suggestions")
|
|
expectIDs(t, "articles", body.articleIDs(), 501)
|
|
if body.Total != 1 || body.Counts["suggestions"] != 1 {
|
|
t.Fatalf("total=%d counts=%v", body.Total, body.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("All filters combine with paging and stay stable", func(t *testing.T) {
|
|
env := fresh(t)
|
|
for story := uint32(21); story <= 24; story++ {
|
|
env.addVersion(localizationVersion{id: story * 10, lang: "en", online: true, title: "Journal extra", updated: localizationDay(2, 9), byline: "Luis Ferrer", noindex: true, storyKey: story})
|
|
}
|
|
seen := map[int]bool{}
|
|
sizes := []int{}
|
|
for page := 1; page <= 3; page++ {
|
|
_, body := env.get(localizationOwnerID, "?group=story&size=5&page="+itoa(uint32(page)))
|
|
if body.Total != 12 {
|
|
t.Fatalf("page %d total = %d", page, body.Total)
|
|
}
|
|
sizes = append(sizes, len(body.Items))
|
|
for _, id := range body.storyIDs() {
|
|
if seen[id] {
|
|
t.Fatalf("story %d repeats", id)
|
|
}
|
|
seen[id] = true
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(sizes, []int{5, 5, 2}) {
|
|
t.Fatalf("page sizes = %v", sizes)
|
|
}
|
|
})
|
|
|
|
t.Run("Sort orders stories, not the page", func(t *testing.T) {
|
|
env := base(t)
|
|
cases := map[string][]int{
|
|
"updated_at:desc": {1, 2, 3, 4, 5, 6, 7, 8},
|
|
"updated_at:asc": {8, 7, 6, 5, 4, 3, 2, 1},
|
|
"title:asc": {1, 2, 3, 5, 6, 7, 8, 4},
|
|
"title:desc": {4, 8, 7, 6, 5, 3, 2, 1},
|
|
"status:asc": {4, 8, 7, 6, 5, 3, 2, 1},
|
|
"created_at:asc": {8, 7, 6, 5, 4, 3, 2, 1},
|
|
}
|
|
for sortValue, want := range cases {
|
|
_, body := env.get(localizationOwnerID, "?group=story&size=100&sort="+sortValue)
|
|
expectIDs(t, sortValue, body.storyIDs(), want...)
|
|
}
|
|
})
|
|
|
|
t.Run("Ties end with story_id descending", func(t *testing.T) {
|
|
env := fresh(t)
|
|
for _, story := range []uint32{11, 12} {
|
|
env.addVersion(localizationVersion{id: story * 10, lang: "en", online: true, title: "Tied", updated: localizationDay(1, 9), byline: "Luis Ferrer", noindex: true, storyKey: story})
|
|
}
|
|
_, body := env.get(localizationOwnerID, "?group=story&size=100")
|
|
expectIDs(t, "tail", body.storyIDs()[8:], 12, 11)
|
|
})
|
|
|
|
t.Run("Counts of the Journal dataset, all languages", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story")
|
|
expectCounts(t, body.Counts, map[string]int{"all": 8, "online": 7, "draft": 4, "suggestions": 0, "missing": 7})
|
|
})
|
|
|
|
t.Run("Counts of the Journal dataset, French", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&lang=fr")
|
|
expectCounts(t, body.Counts, map[string]int{"all": 4, "online": 2, "draft": 2, "suggestions": 0, "missing": 4})
|
|
})
|
|
|
|
t.Run("Counts follow the search", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&q=zinc")
|
|
expectCounts(t, body.Counts, map[string]int{"all": 0, "online": 0, "draft": 0, "suggestions": 0, "missing": 0})
|
|
if len(body.Items) != 0 || body.Total != 0 {
|
|
t.Fatal("expected nothing")
|
|
}
|
|
})
|
|
|
|
t.Run("Counts follow the author filter", func(t *testing.T) {
|
|
env := base(t)
|
|
_, body := env.get(localizationOwnerID, "?group=story&author="+itoa(env.authors["Marta Alves"]))
|
|
expectIDs(t, "stories", body.storyIDs(), 1, 4, 7)
|
|
if body.Total != 3 || body.Counts["all"] != 3 {
|
|
t.Fatalf("total=%d counts=%v", body.Total, body.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("Counts do not move with status or tab", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&lang=fr&status=draft&tab=missing")
|
|
if body.Counts["all"] != 4 || body.Counts["online"] != 2 || body.Counts["draft"] != 2 {
|
|
t.Fatalf("counts = %v", body.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("A reader's counts cover readable versions only", func(t *testing.T) {
|
|
_, body := base(t).get(localizationReaderID, "?group=story")
|
|
if body.Counts["draft"] != 0 || body.Counts["all"] != 7 || body.Counts["missing"] != 0 {
|
|
t.Fatalf("counts = %v", body.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("Total versus counts", func(t *testing.T) {
|
|
_, body := base(t).get(localizationOwnerID, "?group=story&tab=missing")
|
|
if body.Total != body.Counts["missing"] || body.Total != 7 {
|
|
t.Fatalf("total=%d counts=%v", body.Total, body.Counts)
|
|
}
|
|
})
|
|
|
|
t.Run("Missing membership and counts agree with the missing_langs of the rows", func(t *testing.T) {
|
|
env := fresh(t)
|
|
env.addVersion(localizationVersion{id: 31, lang: "", online: true, title: "Journal 31", updated: localizationDay(2, 9), byline: "Luis Ferrer", noindex: true, storyKey: 31})
|
|
env.addVersion(localizationVersion{id: 32, lang: "en", online: true, title: "Journal 32", updated: localizationDay(2, 8), byline: "Luis Ferrer", noindex: true, storyKey: 31})
|
|
env.addVersion(localizationVersion{id: 33, lang: "", online: true, title: "Journal 33", updated: localizationDay(3, 9), byline: "Luis Ferrer", noindex: true, storyKey: 33})
|
|
env.addVersion(localizationVersion{id: 34, lang: "fr", online: false, title: "Journal 34", updated: localizationDay(3, 8), byline: "Luis Ferrer", noindex: true, storyKey: 33})
|
|
env.addVersion(localizationVersion{id: 35, lang: "", online: true, title: "Journal 35", updated: localizationDay(4, 9), byline: "Luis Ferrer", noindex: true, storyKey: 35})
|
|
_, all := env.get(localizationOwnerID, "?group=story&size=100")
|
|
_, missing := env.get(localizationOwnerID, "?group=story&tab=missing&size=100")
|
|
wanted := []int{}
|
|
for _, item := range all.Items {
|
|
if len(stringsOf(item["missing_langs"])) > 0 {
|
|
wanted = append(wanted, int(item["story_id"].(float64)))
|
|
}
|
|
}
|
|
got := missing.storyIDs()
|
|
sort.Ints(wanted)
|
|
sort.Ints(got)
|
|
expectIDs(t, "needs translation", got, wanted...)
|
|
if missing.Total != len(wanted) || missing.Counts["missing"] != len(wanted) || all.Counts["missing"] != len(wanted) {
|
|
t.Fatalf("total=%d counts=%v, want %d", missing.Total, missing.Counts, len(wanted))
|
|
}
|
|
for _, mixed := range []int{31, 33, 35} {
|
|
if missing.item(mixed) != nil {
|
|
t.Fatalf("story %d holding an unassigned version listed as needing translation", mixed)
|
|
}
|
|
}
|
|
for _, tag := range []string{"fr", "de", "ar", "en"} {
|
|
_, notIn := env.get(localizationOwnerID, "?group=story&lang="+tag+"&tab=missing&size=100")
|
|
expected := []int{}
|
|
for _, item := range all.Items {
|
|
if listHasTag(stringsOf(item["missing_langs"]), tag) {
|
|
expected = append(expected, int(item["story_id"].(float64)))
|
|
}
|
|
}
|
|
listed := notIn.storyIDs()
|
|
sort.Ints(expected)
|
|
sort.Ints(listed)
|
|
expectIDs(t, "not in "+tag, listed, expected...)
|
|
if notIn.Total != len(expected) || notIn.Counts["missing"] != len(expected) {
|
|
t.Fatalf("not in %s: total=%d counts=%v, want %d", tag, notIn.Total, notIn.Counts, len(expected))
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("The sites are read once per request", func(t *testing.T) {
|
|
env := base(t)
|
|
env.siteRepo.lists = 0
|
|
_, body := env.get(localizationOwnerID, "?group=story&size=100")
|
|
if len(body.Items) == 0 || env.siteRepo.lists != 1 {
|
|
t.Fatalf("items=%d site lists=%d, want rows and 1", len(body.Items), env.siteRepo.lists)
|
|
}
|
|
})
|
|
|
|
t.Run("An expired token is not an editorial caller", func(t *testing.T) {
|
|
env := base(t)
|
|
expired, err := env.token.Encode(map[string]any{"user_id": localizationOwnerID, "token_version": 0, "exp": time.Now().Add(-time.Hour).Unix()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/article/?group=story", nil)
|
|
req.AddCookie(&http.Cookie{Name: "jwt", Value: expired})
|
|
rec := httptest.NewRecorder()
|
|
env.router.ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusBadRequest || env.repo.calls != 0 {
|
|
t.Fatalf("expected 400 without repository call, got %d calls=%d", rec.Code, env.repo.calls)
|
|
}
|
|
})
|
|
|
|
t.Run("The grouped envelope has the documented keys and never a null list", func(t *testing.T) {
|
|
env := base(t)
|
|
rec, _ := env.get(localizationOwnerID, "?group=story&lang=fr&q=zzzznomatch")
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &raw); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(raw) != 5 || string(raw["items"]) != "[]" {
|
|
t.Fatalf("unexpected empty envelope: %s", rec.Body.String())
|
|
}
|
|
var counts map[string]int
|
|
if err := json.Unmarshal(raw["counts"], &counts); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, key := range []string{"all", "online", "draft", "suggestions", "missing"} {
|
|
if _, ok := counts[key]; !ok {
|
|
t.Fatalf("counts lack %s: %v", key, counts)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Repository failure answers 500", func(t *testing.T) {
|
|
for _, method := range []string{"FindStoryPage", "CountStories", "FindPage"} {
|
|
env := base(t)
|
|
env.repo.failMethod = method
|
|
rec, _ := env.get(localizationOwnerID, "?group=story")
|
|
if rec.Code != http.StatusInternalServerError || rec.Body.String() != failedToRetrieveArticles {
|
|
t.Fatalf("%s: %d %q", method, rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func itoa(value uint32) string {
|
|
return strconv.FormatUint(uint64(value), 10)
|
|
}
|