feat: editorial article list

This commit is contained in:
Florian Sylvain committed 2026-11-02 07:28:44 +01:00
1 parent a0b3c58c52
commit a671b81f80
103 files changed
+6493 -1805

No files matched your search

+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+713
View File
@@ -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
}
+68
View File
@@ -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))
}
}
+1 -1
View File
@@ -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),
+43
View File
@@ -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
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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())
+1 -1
View File
@@ -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()}
+1 -1
View File
@@ -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
+5 -1
View File
@@ -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)
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),
+3 -3
View File
@@ -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 {
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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),