feat: editorial article list
This commit is contained in:
1 parent
a0b3c58c52
commit
a671b81f80
103 files changed
+6493
-1805
No files matched your search
@@ -111,7 +111,7 @@ func newAPIKeyEnvRecording(t *testing.T, recorder audit.Recorder) *apiKeyEnv {
|
||||
Token: env.token,
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(passthroughCreateTransactor(env.articles), renderer, recorder, plainSites(), plainAuthors()),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(env.articles, renderer),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(env.articles, renderer, plainSites()),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(env.articles, renderer, plainSites(), plainLocalization()),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(env.articles, renderer, recorder, plainSites(), plainAuthors(), env.images, newFakeLocalizationStore(env.articles), plainStoryMover(env.articles)),
|
||||
CreateTranslationUseCase: plainTranslation(env.articles),
|
||||
DescribeArticleUseCase: plainDescribe(env.articles),
|
||||
|
||||
@@ -45,7 +45,7 @@ func newBodyFormatRouter(articleRepo *mockArticleRepo, userRepo *mockUserRepo, r
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), renderer, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, renderer),
|
||||
useCases.NewListArticlesUseCase(articleRepo, renderer, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, renderer, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, renderer, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -25,7 +25,7 @@ func newUnlimitedBodyRouter(articleRepo *mockArticleRepo, token *authtoken.Auth)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), renderer, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, renderer),
|
||||
useCases.NewListArticlesUseCase(articleRepo, renderer, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, renderer, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, renderer, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -37,7 +37,7 @@ func newCreatedByEnv(t *testing.T) (*activityHTTPEnv, http.Handler) {
|
||||
AuthenticateAPIKeyUseCase: useCases.NewAuthenticateAPIKeyUseCase(e.keys, e.generator, e.users, e.clock),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(passthroughCreateTransactor(e.articles), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(e.articles, &fakeBodyRenderer{}),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(e.articles, &fakeBodyRenderer{}, plainSites()),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(e.articles, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(e.articles, discardRecorder),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(e.articles, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(e.articles), plainStoryMover(e.articles)),
|
||||
CreateTranslationUseCase: plainTranslation(e.articles),
|
||||
|
||||
@@ -0,0 +1,713 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/api/handlers/article"
|
||||
"RenewCMS/api/pkg/access"
|
||||
"RenewCMS/api/pkg/authtoken"
|
||||
"RenewCMS/api/pkg/claims"
|
||||
domainArticle "RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/site"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
localizationOwnerID uint32 = 1
|
||||
localizationReaderID uint32 = 2
|
||||
localizationWriterID uint32 = 3
|
||||
localizationUnverifiedID uint32 = 4
|
||||
localizationOtherUserID uint32 = 9
|
||||
localizationMalformed uint32 = 999
|
||||
)
|
||||
|
||||
type localizationVersion struct {
|
||||
id uint32
|
||||
lang string
|
||||
online bool
|
||||
title string
|
||||
creator uint32
|
||||
updated time.Time
|
||||
byline string
|
||||
siteKey string
|
||||
noindex bool
|
||||
storyKey uint32
|
||||
}
|
||||
|
||||
type storyListingRepository struct {
|
||||
*persistence.ArticleRepository
|
||||
calls int
|
||||
failMethod string
|
||||
}
|
||||
|
||||
func (r *storyListingRepository) failure(method string) error {
|
||||
r.calls++
|
||||
if r.failMethod == method {
|
||||
return errRepositoryFailure
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *storyListingRepository) FindPage(filters domainArticle.Filters) (domainArticle.PageResult, error) {
|
||||
if err := r.failure("FindPage"); err != nil {
|
||||
return domainArticle.PageResult{}, err
|
||||
}
|
||||
return r.ArticleRepository.FindPage(filters)
|
||||
}
|
||||
|
||||
func (r *storyListingRepository) CountVersions(filters domainArticle.Filters) (domainArticle.Counts, error) {
|
||||
if err := r.failure("CountVersions"); err != nil {
|
||||
return domainArticle.Counts{}, err
|
||||
}
|
||||
return r.ArticleRepository.CountVersions(filters)
|
||||
}
|
||||
|
||||
func (r *storyListingRepository) FindStoryPage(filters domainArticle.StoryFilters) (domainArticle.StoryPage, error) {
|
||||
if err := r.failure("FindStoryPage"); err != nil {
|
||||
return domainArticle.StoryPage{}, err
|
||||
}
|
||||
return r.ArticleRepository.FindStoryPage(filters)
|
||||
}
|
||||
|
||||
func (r *storyListingRepository) CountStories(filters domainArticle.StoryFilters) (domainArticle.StoryCounts, error) {
|
||||
if err := r.failure("CountStories"); err != nil {
|
||||
return domainArticle.StoryCounts{}, err
|
||||
}
|
||||
return r.ArticleRepository.CountStories(filters)
|
||||
}
|
||||
|
||||
type listCountingSites struct {
|
||||
*persistence.SiteRepository
|
||||
lists int
|
||||
}
|
||||
|
||||
func (r *listCountingSites) List() ([]site.Site, error) {
|
||||
r.lists++
|
||||
return r.SiteRepository.List()
|
||||
}
|
||||
|
||||
type localizationListEnv struct {
|
||||
siteRepo *listCountingSites
|
||||
t *testing.T
|
||||
db *gorm.DB
|
||||
repo *storyListingRepository
|
||||
router *chi.Mux
|
||||
token *authtoken.Auth
|
||||
authors map[string]uint32
|
||||
sites map[string]uint32
|
||||
nextID uint32
|
||||
}
|
||||
|
||||
func localizationGrants(userID uint32) (role.GrantSet, bool) {
|
||||
switch userID {
|
||||
case localizationOwnerID:
|
||||
return role.Role{Grants: []role.Grant{
|
||||
{Key: role.KeyContentRead, Scope: role.ScopeAll},
|
||||
{Key: role.KeyContentCreate, Scope: role.ScopeAll},
|
||||
{Key: role.KeyContentUpdate, Scope: role.ScopeAll},
|
||||
{Key: role.KeyContentPublish, Scope: role.ScopeAll},
|
||||
{Key: role.KeyContentDelete, Scope: role.ScopeAll},
|
||||
}}.GrantSet(), true
|
||||
case localizationReaderID:
|
||||
return role.Role{}.GrantSet(), true
|
||||
case localizationWriterID:
|
||||
return role.Role{Grants: []role.Grant{
|
||||
{Key: role.KeyContentRead, Scope: role.ScopeOwn},
|
||||
{Key: role.KeyContentCreate, Scope: role.ScopeOwn},
|
||||
}}.GrantSet(), true
|
||||
}
|
||||
return role.GrantSet{}, false
|
||||
}
|
||||
|
||||
func localizationAccess(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if userID, ok := claims.UserId(r); ok {
|
||||
if grants, known := localizationGrants(userID); known {
|
||||
r = r.WithContext(access.WithCaller(r.Context(), access.Caller{UserID: userID, Grants: grants}))
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func newLocalizationListEnv(t *testing.T, open func(*testing.T) *gorm.DB) *localizationListEnv {
|
||||
t.Helper()
|
||||
db := open(t)
|
||||
repo := &storyListingRepository{ArticleRepository: persistence.NewArticleRepository(db)}
|
||||
siteRepo := &listCountingSites{SiteRepository: persistence.NewSiteRepository(db)}
|
||||
list := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, siteRepo, persistence.NewLocalizationRepository(db))
|
||||
handler := article.NewHandler(nil, nil, list, nil, nil, nil, nil)
|
||||
token := authtoken.New([]byte("test-secret"))
|
||||
router := chi.NewRouter()
|
||||
router.Route("/v1", func(r chi.Router) {
|
||||
r.Route("/article", func(r chi.Router) {
|
||||
r.Use(authtoken.Verifier(token))
|
||||
r.Use(localizationAccess)
|
||||
r.Get("/", handler.ListArticles)
|
||||
})
|
||||
})
|
||||
env := &localizationListEnv{siteRepo: siteRepo, t: t, db: db, repo: repo, router: router, token: token, authors: map[string]uint32{}, sites: map[string]uint32{}}
|
||||
env.seedJournal()
|
||||
return env
|
||||
}
|
||||
|
||||
func (e *localizationListEnv) must(err error) {
|
||||
e.t.Helper()
|
||||
if err != nil {
|
||||
e.t.Fatalf("seed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *localizationListEnv) addSite(key string, defaultLang string, enabled []string, disabled []string) {
|
||||
e.t.Helper()
|
||||
site := entity.Site{Name: key, Key: key, BaseURL: "https://" + key + ".example.com", DefaultRobots: "index"}
|
||||
e.must(e.db.Create(&site).Error)
|
||||
e.sites[key] = site.ID
|
||||
for _, tag := range append(append([]string{}, enabled...), disabled...) {
|
||||
row := entity.SiteLanguage{SiteID: site.ID, Lang: tag, IsDefault: tag == defaultLang, Confirmed: true}
|
||||
e.must(e.db.Create(&row).Error)
|
||||
e.must(e.db.Model(&entity.SiteLanguage{}).Where("site_id = ? AND lang = ?", site.ID, tag).UpdateColumn("enabled", !listHasTag(disabled, tag)).Error)
|
||||
}
|
||||
}
|
||||
|
||||
func listHasTag(tags []string, tag string) bool {
|
||||
for _, candidate := range tags {
|
||||
if candidate == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *localizationListEnv) addAuthor(name string) {
|
||||
e.t.Helper()
|
||||
row := entity.Author{Name: name, Slug: fmt.Sprintf("author-%d", len(e.authors)+1), SocialLinks: "[]"}
|
||||
e.must(e.db.Create(&row).Error)
|
||||
e.authors[name] = row.ID
|
||||
}
|
||||
|
||||
func (e *localizationListEnv) addVersion(version localizationVersion) {
|
||||
e.t.Helper()
|
||||
siteKey := version.siteKey
|
||||
if siteKey == "" {
|
||||
siteKey = "journal"
|
||||
}
|
||||
creator := version.creator
|
||||
if creator == 0 {
|
||||
creator = localizationOwnerID
|
||||
}
|
||||
var story entity.Story
|
||||
if err := e.db.First(&story, version.storyKey).Error; err != nil {
|
||||
e.must(e.db.Create(&entity.Story{ID: version.storyKey}).Error)
|
||||
}
|
||||
row := entity.Article{
|
||||
ID: version.id, Title: version.title, Body: "<p>body</p>", IsOnline: version.online, SiteID: e.sites[siteKey], CreatedBy: creator,
|
||||
Slug: fmt.Sprintf("a-%d", version.id), Noindex: version.noindex, Lang: version.lang, StoryID: version.storyKey,
|
||||
CreatedAt: version.updated.Add(-24 * time.Hour), UpdatedAt: version.updated, ContentRevision: 1, EditRevision: 1,
|
||||
}
|
||||
e.must(e.db.Create(&row).Error)
|
||||
if version.byline != "" {
|
||||
e.must(e.db.Create(&entity.ArticleAuthor{ArticleID: version.id, AuthorID: e.authors[version.byline], Position: 0}).Error)
|
||||
}
|
||||
}
|
||||
|
||||
func localizationDay(day int, hour int) time.Time {
|
||||
return time.Date(2026, time.October, day, hour, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func (e *localizationListEnv) seedJournal() {
|
||||
e.addSite("journal", "en", []string{"en", "fr", "de", "ar"}, []string{"es", "it"})
|
||||
e.addSite("press", "en", []string{"en", "fr"}, nil)
|
||||
e.addSite("atelier", "en", []string{"en", "en-GB", "fr"}, nil)
|
||||
for _, name := range []string{"Marta Alves", "Camille Roche", "Luis Ferrer"} {
|
||||
e.addAuthor(name)
|
||||
}
|
||||
type journalVersion struct {
|
||||
id uint32
|
||||
lang string
|
||||
online bool
|
||||
}
|
||||
stories := []struct {
|
||||
id uint32
|
||||
byline string
|
||||
latest time.Time
|
||||
latestID uint32
|
||||
versions []journalVersion
|
||||
}{
|
||||
{1, "Marta Alves", localizationDay(30, 10), 102, []journalVersion{{101, "en", true}, {102, "fr", false}, {103, "de", true}, {104, "ar", false}}},
|
||||
{2, "Camille Roche", localizationDay(29, 9), 202, []journalVersion{{201, "en", true}, {202, "fr", true}, {203, "de", true}}},
|
||||
{3, "Luis Ferrer", localizationDay(28, 9), 302, []journalVersion{{301, "en", true}, {302, "fr", true}}},
|
||||
{4, "Marta Alves", localizationDay(27, 9), 401, []journalVersion{{401, "en", false}}},
|
||||
{5, "Luis Ferrer", localizationDay(14, 9), 502, []journalVersion{{501, "en", true}, {502, "de", false}}},
|
||||
{6, "Camille Roche", localizationDay(13, 9), 602, []journalVersion{{601, "en", true}, {602, "fr", false}}},
|
||||
{7, "Marta Alves", localizationDay(12, 9), 702, []journalVersion{{701, "en", true}, {702, "de", true}}},
|
||||
{8, "Luis Ferrer", localizationDay(10, 9), 801, []journalVersion{{801, "es", true}}},
|
||||
}
|
||||
for _, story := range stories {
|
||||
for _, version := range story.versions {
|
||||
updated := story.latest
|
||||
if version.id != story.latestID {
|
||||
updated = story.latest.Add(-time.Hour)
|
||||
}
|
||||
e.addVersion(localizationVersion{id: version.id, lang: version.lang, online: version.online, title: fmt.Sprintf("Journal %d", version.id), updated: updated, byline: story.byline, noindex: true, storyKey: story.id})
|
||||
}
|
||||
}
|
||||
e.must(e.db.Model(&entity.Article{}).Where("id = ?", 101).UpdateColumn("title", "How we price a bathroom renovation").Error)
|
||||
e.must(e.db.Model(&entity.Article{}).Where("id = ?", 102).UpdateColumn("title", "Comment nous chiffrons une rénovation").Error)
|
||||
e.must(e.db.Model(&entity.Article{}).Where("id = ?", 401).UpdateColumn("title", "Spring maintenance checklist").Error)
|
||||
e.nextID = 900
|
||||
}
|
||||
|
||||
type storyListBody struct {
|
||||
Items []map[string]any `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
Counts map[string]int `json:"counts"`
|
||||
}
|
||||
|
||||
func (e *localizationListEnv) get(userID uint32, query string) (*httptest.ResponseRecorder, storyListBody) {
|
||||
e.t.Helper()
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/article/"+query, nil)
|
||||
switch userID {
|
||||
case 0:
|
||||
case localizationMalformed:
|
||||
req.AddCookie(&http.Cookie{Name: "jwt", Value: "garbage"})
|
||||
default:
|
||||
setJwtCookie(e.t, req, e.token, userID)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
e.router.ServeHTTP(rec, req)
|
||||
var body storyListBody
|
||||
if rec.Code == http.StatusOK {
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
e.t.Fatalf("decode %s: %v", rec.Body.String(), err)
|
||||
}
|
||||
}
|
||||
return rec, body
|
||||
}
|
||||
|
||||
func (b storyListBody) storyIDs() []int {
|
||||
ids := []int{}
|
||||
for _, item := range b.Items {
|
||||
ids = append(ids, int(item["story_id"].(float64)))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (b storyListBody) articleIDs() []int {
|
||||
ids := []int{}
|
||||
for _, item := range b.Items {
|
||||
ids = append(ids, int(item["id"].(float64)))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (b storyListBody) item(storyID int) map[string]any {
|
||||
for _, item := range b.Items {
|
||||
if int(item["story_id"].(float64)) == storyID {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringsOf(value any) []string {
|
||||
listed, _ := value.([]any)
|
||||
out := []string{}
|
||||
for _, entry := range listed {
|
||||
out = append(out, entry.(string))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func coverageLangs(item map[string]any) []string {
|
||||
langs := []string{}
|
||||
for _, entry := range item["coverage"].([]any) {
|
||||
langs = append(langs, entry.(map[string]any)["lang"].(string))
|
||||
}
|
||||
return langs
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"RenewCMS/internal/domain/article"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
)
|
||||
|
||||
func TestListStoriesPageSuggestionCounts(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
articles []article.Article
|
||||
want int
|
||||
}{
|
||||
{"versions of one story count once", []article.Article{
|
||||
{ID: 1, StoryID: 10, SiteID: 1, IsOnline: true},
|
||||
{ID: 2, StoryID: 10, SiteID: 1, IsOnline: true},
|
||||
}, 1},
|
||||
{"stories without an id count individually", []article.Article{
|
||||
{ID: 1, StoryID: 0, SiteID: 1, IsOnline: true},
|
||||
{ID: 2, StoryID: 0, SiteID: 1, IsOnline: true},
|
||||
{ID: 3, StoryID: 10, SiteID: 1, IsOnline: true},
|
||||
}, 3},
|
||||
{"offline versions are not visible to a reader", []article.Article{
|
||||
{ID: 1, StoryID: 10, SiteID: 1, IsOnline: false},
|
||||
}, 0},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
for _, a := range c.articles {
|
||||
repo.articles[a.ID] = a
|
||||
}
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites(), newFakeLocalizationStore(repo))
|
||||
page, err := uc.ListStoriesPage(useCases.ListStoriesCommand{Page: 1, Size: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if page.Counts.Suggestions != c.want {
|
||||
t.Fatalf("suggestions = %d, want %d", page.Counts.Suggestions, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListArticlesPageSEOFilterAndSuggestionCount(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
for id := uint32(1); id <= 3; id++ {
|
||||
repo.articles[id] = article.Article{ID: id, StoryID: id, SiteID: 1, IsOnline: true}
|
||||
}
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites(), plainLocalization())
|
||||
page, err := uc.ListArticlesPage(useCases.ListArticlesCommand{Page: 2, Size: 2, SEO: article.SEOStatusSuggestions})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if page.Total != 3 || len(page.Items) != 1 || page.Counts.Suggestions != 3 {
|
||||
t.Fatalf("total=%d items=%d suggestions=%d, want 3/1/3", page.Total, len(page.Items), page.Counts.Suggestions)
|
||||
}
|
||||
beyond, _ := uc.ListArticlesPage(useCases.ListArticlesCommand{Page: 3, Size: 2, SEO: article.SEOStatusSuggestions})
|
||||
if len(beyond.Items) != 0 || beyond.Total != 3 {
|
||||
t.Fatalf("beyond: items=%d total=%d, want 0/3", len(beyond.Items), beyond.Total)
|
||||
}
|
||||
none, _ := uc.ListArticlesPage(useCases.ListArticlesCommand{Page: 1, Size: 2, SEO: article.SEOStatusReady})
|
||||
if none.Total != 0 || len(none.Items) != 0 {
|
||||
t.Fatalf("ready: total=%d items=%d, want 0/0", none.Total, len(none.Items))
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ func newArticlePublishingRouter(articleRepo *mockArticleRepo, token *authtoken.A
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -37,6 +37,7 @@ type mockArticleRepo struct {
|
||||
beforeMove func()
|
||||
onGet func(id uint32) error
|
||||
reverseStory bool
|
||||
listCalls int
|
||||
}
|
||||
|
||||
func newMockArticleRepo() *mockArticleRepo {
|
||||
@@ -104,6 +105,7 @@ func (m *mockArticleRepo) FindByFilters(filters article.Filters) ([]article.Arti
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) FindPage(filters article.Filters) (article.PageResult, error) {
|
||||
m.listCalls++
|
||||
if err := m.fail.trigger("FindPage"); err != nil {
|
||||
return article.PageResult{}, err
|
||||
}
|
||||
@@ -124,6 +126,12 @@ func (m *mockArticleRepo) FindPage(filters article.Filters) (article.PageResult,
|
||||
if filters.Status == article.StatusDraft && a.IsOnline {
|
||||
continue
|
||||
}
|
||||
if filters.Lang != nil && a.Lang != *filters.Lang {
|
||||
continue
|
||||
}
|
||||
if filters.AuthorID != nil && !slices.Contains(m.credits[a.ID], *filters.AuthorID) {
|
||||
continue
|
||||
}
|
||||
if filters.Q != "" && !strings.Contains(foldArticleSearch(a.Title), foldArticleSearch(filters.Q)) {
|
||||
continue
|
||||
}
|
||||
@@ -158,6 +166,41 @@ func (m *mockArticleRepo) CountByVisibility(onlineOnly bool) (article.Counts, er
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) CountVersions(filters article.Filters) (article.Counts, error) {
|
||||
m.listCalls++
|
||||
if err := m.fail.trigger("CountVersions"); err != nil {
|
||||
return article.Counts{}, err
|
||||
}
|
||||
counts := article.Counts{}
|
||||
for _, a := range m.articles {
|
||||
if filters.Lang != nil && a.Lang != *filters.Lang {
|
||||
continue
|
||||
}
|
||||
if a.IsOnline {
|
||||
counts.Online++
|
||||
} else if filters.IsOnline == nil && (filters.VisibleTo == nil || a.CreatedBy == *filters.VisibleTo) {
|
||||
counts.Draft++
|
||||
}
|
||||
}
|
||||
counts.All = counts.Online + counts.Draft
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) FindStoryPage(filters article.StoryFilters) (article.StoryPage, error) {
|
||||
m.listCalls++
|
||||
return article.StoryPage{Refs: []article.StoryRef{}}, m.fail.trigger("FindStoryPage")
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) CountStories(filters article.StoryFilters) (article.StoryCounts, error) {
|
||||
m.listCalls++
|
||||
return article.StoryCounts{}, m.fail.trigger("CountStories")
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) ListByStories(storyIDs []uint32) ([]article.Article, error) {
|
||||
m.listCalls++
|
||||
return []article.Article{}, m.fail.trigger("ListByStories")
|
||||
}
|
||||
|
||||
func (m *mockArticleRepo) CountVisibleTo(userID uint32) (article.Counts, error) {
|
||||
if err := m.fail.trigger("CountVisibleTo"); err != nil {
|
||||
return article.Counts{}, err
|
||||
|
||||
@@ -28,7 +28,7 @@ func newArticleTitleRulesRouter(repo *mockArticleRepo, token *authtoken.Auth) *c
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(repo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(repo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(repo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(repo), plainStoryMover(repo)),
|
||||
plainTranslation(repo),
|
||||
|
||||
@@ -22,7 +22,7 @@ func newArticlesListRouter(articleRepo *mockArticleRepo, token *authtoken.Auth)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -578,7 +578,7 @@ func TestArticlesPaginationUseCase(t *testing.T) {
|
||||
t.Run("The typed sort reaches the repository and an absent sort keeps updated_at descending", func(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
seedSortArticles(repo)
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites())
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites(), plainLocalization())
|
||||
sorted, err := uc.ListArticlesPage(useCases.ListArticlesCommand{Format: domainArticle.BodyFormatHTML, Caller: ownerCaller(1), Page: 1, Size: 25, Sort: domainArticle.Sort{Field: domainArticle.SortByTitle, Direction: domainArticle.SortAsc}})
|
||||
if err != nil || sorted.Total != 3 || sorted.Counts.All != 3 {
|
||||
t.Fatalf("unexpected %v %+v", err, sorted)
|
||||
@@ -594,7 +594,7 @@ func TestArticlesPaginationUseCase(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
seedPaginationArticles(repo, 3, 0)
|
||||
repo.fail.failNextWith(method, sentinel)
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites())
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites(), plainLocalization())
|
||||
_, err := uc.ListArticlesPage(useCases.ListArticlesCommand{Format: domainArticle.BodyFormatHTML, Caller: ownerCaller(1), Page: 1, Size: 25})
|
||||
if !errors.Is(err, sentinel) {
|
||||
t.Fatalf("%s: expected sentinel, got %v", method, err)
|
||||
|
||||
@@ -26,7 +26,7 @@ func newEditArticleBodyRouter(articleRepo *mockArticleRepo, token *authtoken.Aut
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -69,7 +69,7 @@ func newMediaEnv(t *testing.T) *mediaEnv {
|
||||
Token: env.token,
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(passthroughCreateTransactor(env.articles), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(env.articles, &fakeBodyRenderer{}),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(env.articles, &fakeBodyRenderer{}, plainSites()),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(env.articles, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(env.articles, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), env.images, newFakeLocalizationStore(env.articles), plainStoryMover(env.articles)),
|
||||
CreateTranslationUseCase: plainTranslation(env.articles),
|
||||
DescribeArticleUseCase: plainDescribe(env.articles),
|
||||
|
||||
@@ -128,7 +128,7 @@ func newNotFoundErrorsRouter(articleRepo *mockArticleRepo, userRepo *mockUserRep
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -44,7 +44,7 @@ func newPublicArticleListingRouter(articleRepo *mockArticleRepo, token *authtoke
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -26,7 +26,7 @@ func newPublicArticleReadRouter(articleRepo *mockArticleRepo, userRepo *mockUser
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -191,7 +191,7 @@ func TestContentScopeArticleUseCases(t *testing.T) {
|
||||
|
||||
t.Run("ListArticlesPage returns the visible set with matching counts", func(t *testing.T) {
|
||||
repo, sites, _, _ := newRepos()
|
||||
list := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, sites)
|
||||
list := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, sites, plainLocalization())
|
||||
cases := []struct {
|
||||
name string
|
||||
caller useCases.ContentCaller
|
||||
@@ -530,7 +530,7 @@ func TestContentScopeListArticlesSiteLookupFailure(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
sites := plainSites()
|
||||
sites.fail.failNextWith("GetByKey", boom)
|
||||
list := useCases.NewListArticlesUseCase(newMockArticleRepo(), &fakeBodyRenderer{}, sites)
|
||||
list := useCases.NewListArticlesUseCase(newMockArticleRepo(), &fakeBodyRenderer{}, sites, plainLocalization())
|
||||
_, err := list.ListArticlesPage(useCases.ListArticlesCommand{Page: 1, Size: 10, SiteKey: &key, Caller: useCases.ContentCaller{UserID: 9, Grants: grants}})
|
||||
if !errors.Is(err, boom) {
|
||||
t.Fatalf("expected the site lookup error, got %v", err)
|
||||
|
||||
@@ -132,7 +132,7 @@ func newRuntimeSettingsContainer(t *testing.T) *api.UseCases {
|
||||
Token: token,
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
CreateTranslationUseCase: plainTranslation(articleRepo),
|
||||
DescribeArticleUseCase: plainDescribe(articleRepo),
|
||||
|
||||
@@ -59,7 +59,7 @@ func newSeoEnv(t *testing.T) *seoEnv {
|
||||
c.UpdateArticleUseCase = useCases.NewUpdateArticleUseCase(p.articles, renderer, recorder, sites, authors, p.images, newFakeLocalizationStore(p.articles), plainStoryMover(p.articles))
|
||||
c.CreateTranslationUseCase = plainTranslation(p.articles)
|
||||
c.DescribeArticleUseCase = plainDescribe(p.articles)
|
||||
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(p.articles, renderer, sites)
|
||||
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(p.articles, renderer, sites, plainLocalization())
|
||||
c.ListMediaUseCase = useCases.NewListMediaUseCase(p.images, p.articles, p.users, newMockVariantFileRepo(), sites, authors, newMockContentTypeRepo(), newMockEntryRepo())
|
||||
c.DeleteMediaUseCase = useCases.NewDeleteMediaUseCase(p.images, newMockImageStorage(), newMockVariantFileRepo(), recorder, sites, authors, p.articles)
|
||||
c.GetMediaUsageUseCase = useCases.NewGetMediaUsageUseCase(p.images, p.articles, sites, authors, newMockContentTypeRepo(), newMockEntryRepo())
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestSeoFilterPerformance(t *testing.T) {
|
||||
if err := db.CreateInBatches(&rows, 100).Error; err != nil {
|
||||
t.Fatalf("seed: %v", err)
|
||||
}
|
||||
list := useCases.NewListArticlesUseCase(persistence.NewArticleRepository(db), &fakeBodyRenderer{}, persistence.NewSiteRepository(db))
|
||||
list := useCases.NewListArticlesUseCase(persistence.NewArticleRepository(db), &fakeBodyRenderer{}, persistence.NewSiteRepository(db), plainLocalization())
|
||||
|
||||
filtered := useCases.ListArticlesCommand{Caller: ownerCaller(1), Page: 1, Size: 25, SEO: article.SEOStatusSuggestions, Sort: article.DefaultSort()}
|
||||
unfiltered := useCases.ListArticlesCommand{Caller: ownerCaller(1), Page: 1, Size: 25, Sort: article.DefaultSort()}
|
||||
|
||||
@@ -132,7 +132,7 @@ func newSeoOutEnv(t *testing.T) *seoOutEnv {
|
||||
c.ResolveArticleSEOUseCase = useCases.NewResolveArticleSEOUseCase(counted, e.sites, e.images)
|
||||
c.CheckArticleSEOUseCase = useCases.NewCheckArticleSEOUseCase(counted, e.sites, e.images)
|
||||
c.ListSitemapUseCase = useCases.NewListSitemapUseCase(counted, e.sites)
|
||||
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(counted, &fakeBodyRenderer{}, e.sites)
|
||||
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(counted, &fakeBodyRenderer{}, e.sites, plainLocalization())
|
||||
router := chi.NewRouter()
|
||||
router.Mount("/v1", api.InitBackendRoutes(c, e.settings))
|
||||
e.router = router
|
||||
|
||||
@@ -330,7 +330,7 @@ func newSiteEnv(t *testing.T) *siteEnv {
|
||||
c.UpdateArticleUseCase = useCases.NewUpdateArticleUseCase(p.articles, renderer, recorder, sites, plainAuthors(), p.images, newFakeLocalizationStore(p.articles), plainStoryMover(p.articles))
|
||||
c.CreateTranslationUseCase = plainTranslation(p.articles)
|
||||
c.DescribeArticleUseCase = plainDescribe(p.articles)
|
||||
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(p.articles, renderer, sites)
|
||||
c.ListArticlesUseCase = useCases.NewListArticlesUseCase(p.articles, renderer, sites, plainLocalization())
|
||||
c.ListMediaUseCase = useCases.NewListMediaUseCase(p.images, p.articles, p.users, newMockVariantFileRepo(), sites, plainAuthors(), newMockContentTypeRepo(), newMockEntryRepo())
|
||||
c.DeleteMediaUseCase = useCases.NewDeleteMediaUseCase(p.images, newMockImageStorage(), newMockVariantFileRepo(), recorder, sites, plainAuthors(), p.articles)
|
||||
c.MergeMediaUseCase = mergeMedia
|
||||
@@ -418,6 +418,10 @@ func stringPointer(value string) *string { return &value }
|
||||
|
||||
func idPointer(value uint32) *uint32 { return &value }
|
||||
|
||||
func plainLocalization() *fakeLocalizationStore {
|
||||
return newFakeLocalizationStore(newMockArticleRepo())
|
||||
}
|
||||
|
||||
func plainSites() *mockSiteRepo {
|
||||
sites := newMockSiteRepo(newMockArticleRepo())
|
||||
sites.seed(1, "Default", "default", true)
|
||||
|
||||
@@ -25,7 +25,7 @@ func newStaleTokenInvalidationRouter(userRepo *mockUserRepo, articleRepo *mockAr
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -192,7 +192,7 @@ func (w *storyWorld) wire() {
|
||||
AuthenticateAPIKeyUseCase: useCases.NewAuthenticateAPIKeyUseCase(keys, nil, w.users, time.Now),
|
||||
CreateArticleUseCase: useCases.NewCreateArticleUseCase(w.transactor, renderer, w.recorder, w.sites, w.authors),
|
||||
GetArticleUseCase: useCases.NewGetArticleUseCase(w.articles, renderer),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(w.articles, renderer, w.sites),
|
||||
ListArticlesUseCase: useCases.NewListArticlesUseCase(w.articles, renderer, w.sites, plainLocalization()),
|
||||
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(w.articles, w.recorder),
|
||||
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(w.articles, renderer, w.recorder, w.sites, w.authors, w.images, w.rows, moveStory),
|
||||
CreateTranslationUseCase: useCases.NewCreateTranslationUseCase(w.transactor, w.articles, w.rows, renderer, w.recorder),
|
||||
|
||||
@@ -199,7 +199,7 @@ func TestListArticlesFiltersByIsOnline(t *testing.T) {
|
||||
|
||||
t.Run("ListArticlesPage unverified", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites())
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites(), plainLocalization())
|
||||
result, err := uc.ListArticlesPage(useCases.ListArticlesCommand{Format: article.BodyFormatHTML, Page: 1, Size: 25, Sort: article.Sort{Field: article.SortByTitle, Direction: article.SortAsc}})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
@@ -209,7 +209,7 @@ func TestListArticlesFiltersByIsOnline(t *testing.T) {
|
||||
|
||||
t.Run("ListArticlesPage verified", func(t *testing.T) {
|
||||
repo := newRepo()
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites())
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites(), plainLocalization())
|
||||
result, err := uc.ListArticlesPage(useCases.ListArticlesCommand{Format: article.BodyFormatHTML, Caller: ownerCaller(1), Page: 1, Size: 25, Sort: article.Sort{Field: article.SortByTitle, Direction: article.SortAsc}})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
@@ -220,7 +220,7 @@ func TestListArticlesFiltersByIsOnline(t *testing.T) {
|
||||
|
||||
func TestListArticlesPageEmptyRepositoryReturnsEmptySlice(t *testing.T) {
|
||||
repo := newMockArticleRepo()
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites())
|
||||
uc := useCases.NewListArticlesUseCase(repo, &fakeBodyRenderer{}, plainSites(), plainLocalization())
|
||||
|
||||
page, err := uc.ListArticlesPage(useCases.ListArticlesCommand{Format: article.BodyFormatHTML, Caller: ownerCaller(1), Page: 1, Size: 25})
|
||||
if err != nil {
|
||||
|
||||
@@ -23,7 +23,7 @@ func newVerifiedAccountRequiredRouter(articleRepo *mockArticleRepo, userRepo *mo
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
@@ -215,7 +215,7 @@ func newDeleteArticleRouter(articleRepo *mockArticleRepo, token *authtoken.Auth)
|
||||
articleHandler := article.NewHandler(
|
||||
useCases.NewCreateArticleUseCase(passthroughCreateTransactor(articleRepo), &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()),
|
||||
useCases.NewGetArticleUseCase(articleRepo, &fakeBodyRenderer{}),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites()),
|
||||
useCases.NewListArticlesUseCase(articleRepo, &fakeBodyRenderer{}, plainSites(), plainLocalization()),
|
||||
useCases.NewDeleteArticleUseCase(articleRepo, discardRecorder),
|
||||
useCases.NewUpdateArticleUseCase(articleRepo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors(), newMockImageRepo(), newFakeLocalizationStore(articleRepo), plainStoryMover(articleRepo)),
|
||||
plainTranslation(articleRepo),
|
||||
|
||||
Reference in new issue
Block a user