348 lines
11 KiB
Go
348 lines
11 KiB
Go
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)
|
|
}
|
|
|
|
func (r *storyListingRepository) ListVersions(ids []uint32) ([]domainArticle.Article, error) {
|
|
if err := r.failure("ListVersions"); err != nil {
|
|
return nil, err
|
|
}
|
|
return r.ArticleRepository.ListVersions(ids)
|
|
}
|
|
|
|
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
|
|
}
|