package test import ( "RenewCMS/api/pkg/apperr" "RenewCMS/internal/domain/audit" "net/http" "slices" "testing" ) const storyDeletePath = "/v1/story/5" func (w *storyWorld) storyVersionIDs() []uint32 { ids := []uint32{} for id, version := range w.articles.articles { if version.StoryID == storyStory { ids = append(ids, id) } } slices.Sort(ids) return ids } func TestStoryDeleteHTTP(t *testing.T) { t.Run("Deleting an article removes every version and the article", func(t *testing.T) { w := newStoryWorld(t) w.send(storyOwner, http.MethodDelete, storyDeletePath, "").expect(t, http.StatusNoContent, "") if remaining := w.storyVersionIDs(); len(remaining) != 0 { t.Fatalf("versions left: %v", remaining) } w.send(storyOwner, http.MethodGet, "/v1/article/10", "").expect(t, http.StatusNotFound, apperr.ArticleNotFoundErrorMessage) deletes := 0 for _, event := range w.recorder.events { if event.Action == audit.ActionArticleDelete && event.Params[audit.ParamStoryID] == storyStory { deletes++ } } if deletes != 4 { t.Errorf("expected one article.delete per version, got %d in %v", deletes, w.recorder.actions()) } }) t.Run("Versions the caller cannot read are deleted with the article", func(t *testing.T) { w := newStoryWorld(t) w.send(storyDeleter, http.MethodDelete, storyDeletePath, "").expect(t, http.StatusNoContent, "") if remaining := w.storyVersionIDs(); len(remaining) != 0 { t.Fatalf("hidden versions left: %v", remaining) } }) t.Run("Deleting an article needs delete permission on every version", func(t *testing.T) { w := newStoryWorld(t) before := w.storyVersionIDs() response := w.send(storyWriter1, http.MethodDelete, storyDeletePath, "") response.expect(t, http.StatusForbidden, apperr.MissingPermissionErrorMessage) if response.header(apperr.MissingPermissionHeader) != "content.delete" || !slices.Equal(w.storyVersionIDs(), before) { t.Fatalf("header %q, versions %v", response.header(apperr.MissingPermissionHeader), w.storyVersionIDs()) } }) t.Run("A failed delete removes nothing", func(t *testing.T) { w := newStoryWorld(t) before := w.storyVersionIDs() w.articles.fail.failNext("DeleteStory") w.send(storyOwner, http.MethodDelete, storyDeletePath, "").expect(t, http.StatusInternalServerError, apperr.DeleteArticleFailedErrorMessage) if !slices.Equal(w.storyVersionIDs(), before) { t.Fatalf("versions changed: %v", w.storyVersionIDs()) } failed := w.eventsOf(audit.ActionArticleDelete) if len(failed) != 1 || failed[0].Outcome != audit.OutcomeFailed || failed[0].Target.ID != "10" { t.Fatalf("one failed article.delete on the oldest readable version expected: %+v", failed) } }) t.Run("An unknown or unreadable article answers the same not found", func(t *testing.T) { w := newStoryWorld(t) w.put(w.stored(12)) hidden := w.stored(11) hidden.StoryID = 77 w.put(hidden) w.send(storyOwner, http.MethodDelete, "/v1/story/9999", "").expect(t, http.StatusNotFound, apperr.ArticleNotFoundErrorMessage) w.send(storyDeleter, http.MethodDelete, "/v1/story/77", "").expect(t, http.StatusNotFound, apperr.ArticleNotFoundErrorMessage) if _, kept := w.articles.articles[11]; !kept { t.Fatal("an unreadable article must not be deleted") } }) t.Run("An invalid id is refused", func(t *testing.T) { w := newStoryWorld(t) w.send(storyOwner, http.MethodDelete, "/v1/story/abc", "").expect(t, http.StatusBadRequest, apperr.IdUint32ErrorMessage) }) }