563 lines
20 KiB
Go
563 lines
20 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/api/pkg/apperr"
|
|
"RenewCMS/internal/domain/author"
|
|
"RenewCMS/internal/domain/image"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func (e *authorEnv) seedEditable() {
|
|
e.authors.seed(2, "Camille Roche", "camille-roche")
|
|
e.authors.update(2, func(a *author.Author) {
|
|
a.Bio = "Writer."
|
|
a.Website = stringPointer("https://camille.example")
|
|
a.AvatarID = idPointer(5)
|
|
a.SocialLinks = []author.SocialLink{{Type: "x", URL: "https://x.com/c"}, {Type: "github", URL: "https://github.com/c"}}
|
|
})
|
|
e.images.images[5] = image.Image{ID: 5, Filename: "avatar.png", Path: "/static/uploadedImages/5.png"}
|
|
}
|
|
|
|
func (e *authorEnv) unchanged(t *testing.T, id uint32, before author.Author) {
|
|
t.Helper()
|
|
if !reflect.DeepEqual(e.authors.authors[id], before) {
|
|
t.Errorf("author %d changed: %+v, was %+v", id, e.authors.authors[id], before)
|
|
}
|
|
}
|
|
|
|
func TestAuthorsBackendEditing(t *testing.T) {
|
|
t.Run("The owner edits any author", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", `{"name": "Camille R.", "slug": "camille-r", "bio": "New.", "website": "https://camille.example/new", "social_links": [{"type": "github", "url": "https://github.com/camille"}]}`)
|
|
|
|
body := decodeAuthorBody(t, rec)
|
|
if rec.Code != http.StatusOK || body["name"] != "Camille R." || body["slug"] != "camille-r" || body["bio"] != "New." || body["website"] != "https://camille.example/new" {
|
|
t.Errorf("status %d body %v", rec.Code, body)
|
|
}
|
|
if links := e.authors.authors[2].SocialLinks; len(links) != 1 || links[0].Type != "github" {
|
|
t.Errorf("links = %+v", links)
|
|
}
|
|
})
|
|
|
|
t.Run("Absent fields are untouched", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
before := e.authors.authors[2]
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", `{"name": "Camille R."}`)
|
|
|
|
expected := before
|
|
expected.Name = "Camille R."
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
e.unchanged(t, 2, expected)
|
|
})
|
|
|
|
t.Run("Nullable fields are cleared", func(t *testing.T) {
|
|
cases := []struct {
|
|
body string
|
|
assert func(a author.Author) bool
|
|
}{
|
|
{`{"avatar_id": null}`, func(a author.Author) bool { return a.AvatarID == nil }},
|
|
{`{"website": null}`, func(a author.Author) bool { return a.Website == nil }},
|
|
{`{"website": ""}`, func(a author.Author) bool { return a.Website == nil }},
|
|
{`{"social_links": []}`, func(a author.Author) bool { return len(a.SocialLinks) == 0 }},
|
|
{`{"bio": ""}`, func(a author.Author) bool { return a.Bio == "" }},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.body, func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", c.body)
|
|
|
|
if rec.Code != http.StatusOK || !c.assert(e.authors.authors[2]) {
|
|
t.Errorf("status %d stored %+v", rec.Code, e.authors.authors[2])
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("The avatar is set to an existing image", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
e.authors.update(2, func(a *author.Author) { a.AvatarID = nil })
|
|
|
|
e.owner(t, http.MethodPatch, "/v1/author/2", `{"avatar_id": 5}`)
|
|
|
|
if avatar := e.authors.authors[2].AvatarID; avatar == nil || *avatar != 5 {
|
|
t.Errorf("avatar = %v", avatar)
|
|
}
|
|
})
|
|
|
|
t.Run("Social links are replaced as a whole", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
|
|
e.owner(t, http.MethodPatch, "/v1/author/2", `{"social_links": [{"type": "x", "url": "https://x.com/a"}]}`)
|
|
|
|
if !reflect.DeepEqual(e.authors.authors[2].SocialLinks, []author.SocialLink{{Type: "x", URL: "https://x.com/a"}}) {
|
|
t.Errorf("links = %+v", e.authors.authors[2].SocialLinks)
|
|
}
|
|
})
|
|
|
|
t.Run("Editing is validated like creation", func(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
body string
|
|
status int
|
|
token string
|
|
}{
|
|
{"empty name", `{"name": ""}`, 400, ""},
|
|
{"bad slug", `{"slug": "Bad Slug"}`, 400, "invalid_slug"},
|
|
{"empty slug", `{"slug": ""}`, 400, "invalid_slug"},
|
|
{"long bio", `{"bio": "` + strings.Repeat("a", 281) + `"}`, 422, "bio_too_long"},
|
|
{"http website", `{"website": "http://example.com"}`, 422, "invalid_url"},
|
|
{"six links", `{"social_links": [` + strings.TrimSuffix(strings.Repeat(`{"type": "x", "url": "https://x.com/a"},`, 6), ",") + `]}`, 422, "too_many_links"},
|
|
{"unknown avatar", `{"avatar_id": 999}`, 400, "image_not_found"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
before := e.authors.authors[2]
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", c.body)
|
|
|
|
if rec.Code != c.status || (c.token != "" && bodyText(rec) != c.token) || bodyText(rec) == "" {
|
|
t.Errorf("status %d body %q", rec.Code, bodyText(rec))
|
|
}
|
|
e.unchanged(t, 2, before)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Keeping the own slug is not a collision", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
if rec := e.owner(t, http.MethodPatch, "/v1/author/2", `{"slug": "camille-roche", "bio": "x"}`); rec.Code != http.StatusOK {
|
|
t.Errorf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("Taking another author's slug is refused", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
e.authors.seed(3, "Zoe", "zoe-lambert")
|
|
before := e.authors.authors[2]
|
|
|
|
expectAuthorBody(t, e.owner(t, http.MethodPatch, "/v1/author/2", `{"slug": "zoe-lambert"}`), http.StatusConflict, "author_slug_taken")
|
|
e.unchanged(t, 2, before)
|
|
})
|
|
|
|
t.Run("An empty or unknown update is refused", func(t *testing.T) {
|
|
for _, body := range []string{`{}`, `{"colour": "x"}`, `{"name": null}`} {
|
|
t.Run(body, func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
before := e.authors.authors[2]
|
|
|
|
expectAuthorBody(t, e.owner(t, http.MethodPatch, "/v1/author/2", body), http.StatusBadRequest, apperr.BodyErrorMessage)
|
|
e.unchanged(t, 2, before)
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("A missing author is not found", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
expectAuthorBody(t, e.owner(t, http.MethodPatch, "/v1/author/999", `{"name": "X"}`), http.StatusNotFound, "author_not_found")
|
|
})
|
|
|
|
t.Run("A malformed id is refused", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
expectAuthorBody(t, e.owner(t, http.MethodPatch, "/v1/author/abc", `{"name": "X"}`), http.StatusBadRequest, apperr.IdUint32ErrorMessage)
|
|
})
|
|
|
|
t.Run("A no-op edit records nothing", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", `{"name": "Camille Roche", "slug": "camille-roche", "bio": "Writer."}`)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d", rec.Code)
|
|
}
|
|
e.noEvent(t)
|
|
})
|
|
|
|
t.Run("A role without author rights edits only its own author", func(t *testing.T) {
|
|
cases := []struct {
|
|
id uint32
|
|
status int
|
|
body string
|
|
}{
|
|
{4, 200, ""},
|
|
{2, 403, "author_rights"},
|
|
{999, 404, "author_not_found"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(fmt.Sprint(c.id), func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
e.authors.seedLinked(4, "Edith", "edith", authorEditorID)
|
|
|
|
rec := e.as(t, authorEditorID, http.MethodPatch, fmt.Sprintf("/v1/author/%d", c.id), `{"bio": "Updated."}`)
|
|
|
|
if rec.Code != c.status || (c.body != "" && bodyText(rec) != c.body) {
|
|
t.Errorf("status %d body %q", rec.Code, bodyText(rec))
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("Own author edits include name, slug and avatar", func(t *testing.T) {
|
|
for _, body := range []string{`{"name": "Marta A."}`, `{"slug": "marta-a"}`, `{"avatar_id": 5}`, `{"social_links": []}`} {
|
|
t.Run(body, func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
e.authors.seedLinked(4, "Edith", "edith", authorEditorID)
|
|
if rec := e.as(t, authorEditorID, http.MethodPatch, "/v1/author/4", body); rec.Code != http.StatusOK {
|
|
t.Errorf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("A role without author rights cannot change the link", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seedLinked(4, "Edith", "edith", authorEditorID)
|
|
|
|
expectAuthorBody(t, e.as(t, authorEditorID, http.MethodPatch, "/v1/author/4", `{"user_id": null}`), http.StatusForbidden, "author_rights")
|
|
if e.authors.authors[4].UserID == nil {
|
|
t.Errorf("the author was unlinked")
|
|
}
|
|
})
|
|
|
|
t.Run("An API key cannot edit an author", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
expectAuthorBody(t, e.viaKey(t, http.MethodPatch, "/v1/author/2", `{"bio": "x"}`), http.StatusForbidden, sessionRefusal)
|
|
})
|
|
|
|
t.Run("The response carries the email for owner and admin only", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seedLinked(4, "Edith", "edith", authorEditorID)
|
|
|
|
own := e.as(t, authorEditorID, http.MethodPatch, "/v1/author/4", `{"bio": "Updated."}`)
|
|
admin := e.as(t, apiKeyDevId, http.MethodPatch, "/v1/author/4", `{"bio": "Again."}`)
|
|
|
|
if strings.Contains(own.Body.String(), "user_email") || !strings.Contains(admin.Body.String(), "edith@example.com") {
|
|
t.Errorf("own %s admin %s", own.Body.String(), admin.Body.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthorsBackendLinking(t *testing.T) {
|
|
t.Run("The owner links a user to a guest author", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "Camille Roche", "camille-roche")
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", fmt.Sprintf(`{"user_id": %d}`, authorEditorID))
|
|
|
|
body := decodeAuthorBody(t, rec)
|
|
if rec.Code != http.StatusOK || body["user_id"] != float64(authorEditorID) || body["is_linked"] != true || body["user_email"] != "edith@example.com" {
|
|
t.Errorf("status %d body %v", rec.Code, body)
|
|
}
|
|
})
|
|
|
|
t.Run("A user cannot have two authors", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "Camille Roche", "camille-roche")
|
|
e.authors.seedLinked(4, "Edith", "edith", authorEditorID)
|
|
before := e.authors.authors[2]
|
|
|
|
expectAuthorBody(t, e.owner(t, http.MethodPatch, "/v1/author/2", fmt.Sprintf(`{"user_id": %d}`, authorEditorID)), http.StatusConflict, "user_already_linked")
|
|
e.unchanged(t, 2, before)
|
|
})
|
|
|
|
t.Run("A linked author must be unlinked before it can be relinked", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seedLinked(2, "Camille Roche", "camille-roche", authorEditorID)
|
|
|
|
expectAuthorBody(t, e.owner(t, http.MethodPatch, "/v1/author/2", fmt.Sprintf(`{"user_id": %d}`, authorOtherEditorID)), http.StatusConflict, "author_already_linked")
|
|
})
|
|
|
|
t.Run("Linking the user it already has is a no-op", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seedLinked(2, "Camille Roche", "camille-roche", authorEditorID)
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", fmt.Sprintf(`{"user_id": %d}`, authorEditorID))
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d", rec.Code)
|
|
}
|
|
e.noEvent(t)
|
|
})
|
|
|
|
t.Run("An unknown user cannot be linked", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "Camille Roche", "camille-roche")
|
|
expectAuthorBody(t, e.owner(t, http.MethodPatch, "/v1/author/2", `{"user_id": 999}`), http.StatusNotFound, "user_not_found")
|
|
})
|
|
|
|
t.Run("The owner unlinks a user", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seedLinked(2, "Camille Roche", "camille-roche", authorEditorID)
|
|
e.seedOnlineArticle(10, "Spring release", 2)
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", `{"user_id": null}`)
|
|
|
|
body := decodeAuthorBody(t, rec)
|
|
if _, present := body["user_email"]; rec.Code != http.StatusOK || body["user_id"] != nil || body["is_linked"] != false || present {
|
|
t.Errorf("status %d body %v", rec.Code, body)
|
|
}
|
|
if !reflect.DeepEqual(e.creditsOf(10), []uint32{2}) {
|
|
t.Errorf("credits = %v", e.creditsOf(10))
|
|
}
|
|
if rec := e.as(t, authorEditorID, http.MethodPost, "/v1/author/me", ""); rec.Code != http.StatusCreated {
|
|
t.Errorf("the user cannot create an author again: %d", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Unlinking a guest author is a no-op", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "Camille Roche", "camille-roche")
|
|
|
|
rec := e.owner(t, http.MethodPatch, "/v1/author/2", `{"user_id": null}`)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d", rec.Code)
|
|
}
|
|
e.noEvent(t)
|
|
})
|
|
|
|
t.Run("A new article by the unlinked user no longer starts with the author", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seedLinked(2, "Camille Roche", "camille-roche", authorEditorID)
|
|
e.owner(t, http.MethodPatch, "/v1/author/2", `{"user_id": null}`)
|
|
|
|
rec := e.as(t, authorEditorID, http.MethodPost, "/v1/article", `{"title": "Spring release", "body": "Hello"}`)
|
|
|
|
if body := decodeAuthorBody(t, rec); rec.Code != http.StatusCreated || len(body["authors"].([]any)) != 0 {
|
|
t.Errorf("status %d body %v", rec.Code, body)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAuthorsBackendDeleting(t *testing.T) {
|
|
t.Run("An author without articles is deleted", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "Camille Roche", "camille-roche")
|
|
|
|
if rec := e.owner(t, http.MethodDelete, "/v1/author/2", ""); rec.Code != http.StatusNoContent {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if _, exists := e.authors.authors[2]; exists {
|
|
t.Errorf("the author still exists")
|
|
}
|
|
})
|
|
|
|
t.Run("An author with articles needs a destination", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "Camille Roche", "camille-roche")
|
|
for id := uint32(10); id < 13; id++ {
|
|
e.seedOnlineArticle(id, "Article", 2)
|
|
}
|
|
|
|
expectAuthorBody(t, e.owner(t, http.MethodDelete, "/v1/author/2", ""), http.StatusConflict, "author_has_articles")
|
|
if _, exists := e.authors.authors[2]; !exists || len(e.authors.deleteCalls) != 0 {
|
|
t.Errorf("the author changed")
|
|
}
|
|
})
|
|
|
|
t.Run("The articles are reassigned to another author", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "Camille Roche", "camille-roche")
|
|
e.authors.seed(3, "Zoe", "zoe")
|
|
for id := uint32(10); id < 13; id++ {
|
|
e.seedOnlineArticle(id, "Article", 2)
|
|
}
|
|
|
|
if rec := e.owner(t, http.MethodDelete, "/v1/author/2?reassign_to=3", ""); rec.Code != http.StatusNoContent {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
for id := uint32(10); id < 13; id++ {
|
|
if !reflect.DeepEqual(e.creditsOf(id), []uint32{3}) {
|
|
t.Errorf("credits of %d = %v", id, e.creditsOf(id))
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("The reassigned author takes the position of the deleted one", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "B", "b")
|
|
e.authors.seed(3, "C", "c")
|
|
e.authors.seed(5, "E", "e")
|
|
e.authors.seed(6, "F", "f")
|
|
e.seedOnlineArticle(10, "Article", 5, 2, 6)
|
|
|
|
e.owner(t, http.MethodDelete, "/v1/author/2?reassign_to=3", "")
|
|
|
|
if !reflect.DeepEqual(e.creditsOf(10), []uint32{5, 3, 6}) {
|
|
t.Errorf("credits = %v", e.creditsOf(10))
|
|
}
|
|
})
|
|
|
|
t.Run("A reassignment never duplicates an author on an article", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "B", "b")
|
|
e.authors.seed(3, "C", "c")
|
|
e.seedOnlineArticle(10, "Article", 2, 3)
|
|
|
|
e.owner(t, http.MethodDelete, "/v1/author/2?reassign_to=3", "")
|
|
|
|
if !reflect.DeepEqual(e.creditsOf(10), []uint32{3}) {
|
|
t.Errorf("credits = %v", e.creditsOf(10))
|
|
}
|
|
})
|
|
|
|
t.Run("Nobody removes the byline", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "B", "b")
|
|
e.authors.seed(5, "E", "e")
|
|
e.authors.seed(6, "F", "f")
|
|
e.seedOnlineArticle(10, "Alone", 2)
|
|
e.seedOnlineArticle(11, "Shared", 5, 2, 6)
|
|
|
|
if rec := e.owner(t, http.MethodDelete, "/v1/author/2?reassign_to=none", ""); rec.Code != http.StatusNoContent {
|
|
t.Fatalf("status %d: %s", rec.Code, rec.Body.String())
|
|
}
|
|
if len(e.creditsOf(10)) != 0 || !reflect.DeepEqual(e.creditsOf(11), []uint32{5, 6}) {
|
|
t.Errorf("credits = %v %v", e.creditsOf(10), e.creditsOf(11))
|
|
}
|
|
if _, exists := e.articles.articles[10]; !exists {
|
|
t.Errorf("the article was removed")
|
|
}
|
|
})
|
|
|
|
t.Run("Reassign and delete are all-or-nothing", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "B", "b")
|
|
e.authors.seed(3, "C", "c")
|
|
e.seedOnlineArticle(10, "Article", 2)
|
|
e.authors.fail.failNext("DeleteMovingCredits")
|
|
|
|
rec := e.owner(t, http.MethodDelete, "/v1/author/2?reassign_to=3", "")
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Errorf("status %d", rec.Code)
|
|
}
|
|
if _, exists := e.authors.authors[2]; !exists || !reflect.DeepEqual(e.creditsOf(10), []uint32{2}) {
|
|
t.Errorf("state changed")
|
|
}
|
|
})
|
|
|
|
t.Run("The credits are moved by one repository operation", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "B", "b")
|
|
e.authors.seed(3, "C", "c")
|
|
e.seedOnlineArticle(10, "Article", 2)
|
|
|
|
e.owner(t, http.MethodDelete, "/v1/author/2?reassign_to=3", "")
|
|
|
|
if len(e.authors.deleteCalls) != 1 || e.authors.deleteCalls[0].id != 2 || e.authors.deleteCalls[0].reassignTo == nil || *e.authors.deleteCalls[0].reassignTo != 3 {
|
|
t.Errorf("calls = %+v", e.authors.deleteCalls)
|
|
}
|
|
})
|
|
|
|
t.Run("An unusable destination", func(t *testing.T) {
|
|
cases := []struct {
|
|
param string
|
|
status int
|
|
body string
|
|
}{
|
|
{"999", 404, "author_not_found"},
|
|
{"2", 400, "invalid_reassign"},
|
|
{"abc", 400, "invalid_reassign"},
|
|
{"-1", 400, "invalid_reassign"},
|
|
{"", 400, "invalid_reassign"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.param, func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "B", "b")
|
|
e.seedOnlineArticle(10, "Article", 2)
|
|
|
|
expectAuthorBody(t, e.owner(t, http.MethodDelete, "/v1/author/2?reassign_to="+c.param, ""), c.status, c.body)
|
|
if _, exists := e.authors.authors[2]; !exists || !reflect.DeepEqual(e.creditsOf(10), []uint32{2}) {
|
|
t.Errorf("state changed")
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("A linked author can be deleted and the account is untouched", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seedLinked(2, "Edith", "edith", authorEditorID)
|
|
|
|
e.owner(t, http.MethodDelete, "/v1/author/2", "")
|
|
|
|
if _, exists := e.users.users[authorEditorID]; !exists {
|
|
t.Errorf("the user was removed")
|
|
}
|
|
if rec := e.as(t, authorEditorID, http.MethodPost, "/v1/author/me", ""); rec.Code != http.StatusCreated {
|
|
t.Errorf("status %d", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("A missing author is not found", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
expectAuthorBody(t, e.owner(t, http.MethodDelete, "/v1/author/999", ""), http.StatusNotFound, "author_not_found")
|
|
})
|
|
|
|
t.Run("An unused reassign_to is ignored", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "B", "b")
|
|
e.authors.seed(3, "C", "c")
|
|
before := e.authors.authors[3]
|
|
|
|
if rec := e.owner(t, http.MethodDelete, "/v1/author/2?reassign_to=3", ""); rec.Code != http.StatusNoContent {
|
|
t.Fatalf("status %d", rec.Code)
|
|
}
|
|
e.unchanged(t, 3, before)
|
|
})
|
|
|
|
t.Run("Other roles and keys cannot delete", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seedLinked(2, "Edith", "edith", authorEditorID)
|
|
expectAuthorBody(t, e.as(t, authorEditorID, http.MethodDelete, "/v1/author/2", ""), http.StatusForbidden, ownerOrAdminRefusal)
|
|
expectAuthorBody(t, e.viaKey(t, http.MethodDelete, "/v1/author/2", ""), http.StatusForbidden, sessionRefusal)
|
|
if _, exists := e.authors.authors[2]; !exists {
|
|
t.Errorf("the author was deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("An admin can delete", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.authors.seed(2, "B", "b")
|
|
if rec := e.as(t, apiKeyDevId, http.MethodDelete, "/v1/author/2", ""); rec.Code != http.StatusNoContent {
|
|
t.Errorf("status %d", rec.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("Refused writes change nothing", func(t *testing.T) {
|
|
e := newAuthorEnv(t)
|
|
e.seedEditable()
|
|
before := e.authors.snapshot()
|
|
|
|
expectAuthorBody(t, e.as(t, authorEditorID, http.MethodPost, "/v1/author/", `{"name": "X"}`), http.StatusForbidden, ownerOrAdminRefusal)
|
|
expectAuthorBody(t, e.viaKey(t, http.MethodPost, "/v1/author/", `{"name": "X"}`), http.StatusForbidden, sessionRefusal)
|
|
expectAuthorBody(t, e.as(t, authorEditorID, http.MethodPatch, "/v1/author/2", `{"bio": "x"}`), http.StatusForbidden, "author_rights")
|
|
if !reflect.DeepEqual(e.authors.snapshot(), before) {
|
|
t.Errorf("authors changed")
|
|
}
|
|
})
|
|
}
|