232 lines
8.1 KiB
Go
232 lines
8.1 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/article"
|
|
"RenewCMS/internal/domain/audit"
|
|
"RenewCMS/internal/domain/author"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"slices"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func exportedArticleIDs(t *testing.T, f *exportFixture) []float64 {
|
|
t.Helper()
|
|
files := openExportArchive(t, f.export(t, exportMartaID))
|
|
ids := []float64{}
|
|
for _, raw := range exportList(t, exportJSON(t, files, "authored-content.json"), "articles") {
|
|
ids = append(ids, raw.(map[string]any)["id"].(float64))
|
|
}
|
|
slices.Sort(ids)
|
|
return ids
|
|
}
|
|
|
|
func TestAccountExportDataArticleScope(t *testing.T) {
|
|
t.Run("a draft by someone else credited to the user is excluded", func(t *testing.T) {
|
|
f := newExportFixture(t)
|
|
f.authors.seedLinked(1, "Marta Alves", "marta-alves", exportMartaID)
|
|
f.articles.articles[4] = article.Article{ID: 4, Title: "Ines draft", CreatedBy: exportInesID}
|
|
f.articles.credit(4, 1)
|
|
|
|
if ids := exportedArticleIDs(t, f); !slices.Equal(ids, []float64{1, 2}) {
|
|
t.Errorf("article ids %v", ids)
|
|
}
|
|
})
|
|
t.Run("a published article by someone else credited to the user is included", func(t *testing.T) {
|
|
f := newExportFixture(t)
|
|
f.authors.seedLinked(1, "Marta Alves", "marta-alves", exportMartaID)
|
|
f.articles.articles[4] = article.Article{ID: 4, Title: "Ines published", CreatedBy: exportInesID, IsOnline: true}
|
|
f.articles.credit(4, 1)
|
|
|
|
if ids := exportedArticleIDs(t, f); !slices.Equal(ids, []float64{1, 2, 4}) {
|
|
t.Errorf("article ids %v", ids)
|
|
}
|
|
})
|
|
t.Run("the user's own draft is included", func(t *testing.T) {
|
|
f := newExportFixture(t)
|
|
|
|
if ids := exportedArticleIDs(t, f); !slices.Equal(ids, []float64{1, 2}) || f.articles.articles[2].IsOnline {
|
|
t.Errorf("article ids %v", ids)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAccountExportDataCoAuthorsAreReduced(t *testing.T) {
|
|
f := newExportFixture(t)
|
|
f.authors.seedLinked(1, "Marta Alves", "marta-alves", exportMartaID)
|
|
coAuthor := f.authors.seed(2, "Dario Costa", "dario-costa")
|
|
coAuthor.Bio = "Plasterer from Porto"
|
|
coAuthor.Website = new(string)
|
|
coAuthor.AvatarID = idPointer(9)
|
|
coAuthor.SocialLinks = []author.SocialLink{{URL: "https://example.com/dario"}}
|
|
f.authors.authors[2] = coAuthor
|
|
f.articles.credit(1, 1, 2)
|
|
|
|
files := openExportArchive(t, f.export(t, exportMartaID))
|
|
|
|
var keys []string
|
|
collectKeys(exportJSON(t, files, "authored-content.json"), &keys)
|
|
for _, key := range keys {
|
|
if slices.Contains([]string{"bio", "website", "social_links", "avatar_id"}, key) {
|
|
t.Errorf("authored-content.json exposes %q", key)
|
|
}
|
|
}
|
|
first := exportList(t, exportJSON(t, files, "authored-content.json"), "articles")[0].(map[string]any)
|
|
credits := first["authors"].([]any)
|
|
if len(credits) != 2 || credits[1].(map[string]any)["name"] != "Dario Costa" || credits[1].(map[string]any)["slug"] != "dario-costa" {
|
|
t.Errorf("credits %v", credits)
|
|
}
|
|
profile := exportJSON(t, files, "profile.json")["author"].(map[string]any)
|
|
if _, ok := profile["social_links"]; !ok {
|
|
t.Errorf("profile.json author must stay complete: %v", profile)
|
|
}
|
|
}
|
|
|
|
func TestEventConcernsOtherPerson(t *testing.T) {
|
|
ownAuthor := idPointer(5)
|
|
targeted := func(kind string, id uint32) audit.Event {
|
|
event := audit.NewEvent("x", audit.CategoryUsers)
|
|
event.Target = audit.Target{Type: kind, ID: strconv.Itoa(int(id))}
|
|
return event
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
event audit.Event
|
|
own *uint32
|
|
want bool
|
|
}{
|
|
{"own author", targeted(audit.TargetAuthor, 5), ownAuthor, false},
|
|
{"another author", targeted(audit.TargetAuthor, 6), ownAuthor, true},
|
|
{"an author when the user has none", targeted(audit.TargetAuthor, 5), nil, true},
|
|
{"an author target without an id", audit.Event{Target: audit.Target{Type: audit.TargetAuthor, Name: "Dario"}}, ownAuthor, true},
|
|
{"the user themself", targeted(audit.TargetUser, 7), ownAuthor, false},
|
|
{"another user", targeted(audit.TargetUser, 8), ownAuthor, true},
|
|
{"an invite", targeted(audit.TargetInvite, 3), ownAuthor, true},
|
|
{"an article", targeted(audit.TargetArticle, 3), ownAuthor, false},
|
|
{"no target", audit.Event{}, nil, false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := tc.event.ConcernsOtherPerson(7, tc.own); got != tc.want {
|
|
t.Errorf("got %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAccountExportDataDropsEventsOnOtherAuthors(t *testing.T) {
|
|
f := newExportFixture(t)
|
|
f.authors.seedLinked(1, "Marta Alves", "marta-alves", exportMartaID)
|
|
own := audit.NewEvent(audit.ActionAuthorEdit, audit.CategoryAuthors)
|
|
own.ID, own.Actor = 200, audit.Actor{Type: audit.ActorUser, ID: idPointer(exportMartaID)}
|
|
own.Target = audit.NewTarget(audit.TargetAuthor, 1, "Marta Alves")
|
|
other := own
|
|
other.ID = 201
|
|
other.Target = audit.NewTarget(audit.TargetAuthor, 2, "Dario Costa")
|
|
f.events.events = append(f.events.events, own, other)
|
|
|
|
files := openExportArchive(t, f.export(t, exportMartaID))
|
|
|
|
ids := []float64{}
|
|
for _, raw := range exportList(t, exportJSON(t, files, "account-activity.json"), "events") {
|
|
ids = append(ids, raw.(map[string]any)["id"].(float64))
|
|
}
|
|
if !slices.Contains(ids, 200) || slices.Contains(ids, 201) {
|
|
t.Errorf("event ids %v", ids)
|
|
}
|
|
}
|
|
|
|
func TestAccountExportDataEncodesStraightIntoTheArchive(t *testing.T) {
|
|
f := newExportFixture(t)
|
|
|
|
files := openExportArchive(t, f.export(t, exportMartaID))
|
|
|
|
for name, content := range files {
|
|
if !bytes.HasSuffix(content, []byte("}\n")) || bytes.HasSuffix(content, []byte("\n\n")) || !bytes.HasPrefix(content, []byte("{\n \"")) {
|
|
t.Errorf("%s is not indented JSON ending with a single newline", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAccountExportDataConcurrencyCap(t *testing.T) {
|
|
newBlocked := func(t *testing.T) (*exportFixture, chan struct{}, chan struct{}) {
|
|
f := newExportFixtureWithCap(t, 1)
|
|
entered := make(chan struct{}, 1)
|
|
release := make(chan struct{})
|
|
once := false
|
|
f.images.onListByUploader = func() {
|
|
if once {
|
|
return
|
|
}
|
|
once = true
|
|
entered <- struct{}{}
|
|
<-release
|
|
}
|
|
return f, entered, release
|
|
}
|
|
t.Run("a build waits for a free slot and gives up when its request ends", func(t *testing.T) {
|
|
f, entered, release := newBlocked(t)
|
|
firstDone := make(chan *httptest.ResponseRecorder)
|
|
go func() { firstDone <- f.export(t, exportMartaID) }()
|
|
<-entered
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
|
|
defer cancel()
|
|
req := httptest.NewRequestWithContext(ctx, http.MethodGet, exportRoute, nil)
|
|
req.AddCookie(displayNameCookie(t, f.token, exportInesID, exportTokenVer))
|
|
waiting := httptest.NewRecorder()
|
|
|
|
f.router.ServeHTTP(waiting, req)
|
|
|
|
if waiting.Code != http.StatusServiceUnavailable {
|
|
t.Errorf("waiting build = %d, want 503", waiting.Code)
|
|
}
|
|
close(release)
|
|
if first := <-firstDone; first.Code != http.StatusOK {
|
|
t.Errorf("first build = %d", first.Code)
|
|
}
|
|
if len(f.recorder.events) != 1 {
|
|
t.Errorf("events %d, want only the first build's", len(f.recorder.events))
|
|
}
|
|
if retried := f.export(t, exportInesID); retried.Code != http.StatusOK {
|
|
t.Errorf("refused user must keep the slot free, got %d", retried.Code)
|
|
}
|
|
})
|
|
t.Run("the slot is released when a build fails", func(t *testing.T) {
|
|
f := newExportFixtureWithCap(t, 1)
|
|
f.images.fail.failNext("ListByUploader")
|
|
|
|
failed := f.export(t, exportMartaID)
|
|
next := f.export(t, exportMartaID)
|
|
|
|
if failed.Code != http.StatusInternalServerError || next.Code != http.StatusOK {
|
|
t.Errorf("failed %d then %d", failed.Code, next.Code)
|
|
}
|
|
})
|
|
t.Run("the use case returns the context error", func(t *testing.T) {
|
|
f, entered, release := newBlocked(t)
|
|
usecase := useCases.NewExportUserDataUseCase(f.users, f.roles, f.authors, f.keys, f.articles, f.entries, f.types, f.images, f.events, f.clock.now, f.recorder, 1)
|
|
f.images.onListByUploader = func() { entered <- struct{}{}; <-release }
|
|
done := make(chan error)
|
|
go func() { _, err := usecase.Export(context.Background(), exportMartaID); done <- err }()
|
|
<-entered
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := usecase.Export(ctx, exportInesID)
|
|
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Errorf("err %v", err)
|
|
}
|
|
close(release)
|
|
if err := <-done; err != nil {
|
|
t.Errorf("first build %v", err)
|
|
}
|
|
})
|
|
}
|