231 lines
8.2 KiB
Go
231 lines
8.2 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/api/handlers/article"
|
|
"RenewCMS/api/middleware"
|
|
"RenewCMS/api/pkg/apperr"
|
|
validator "RenewCMS/api/pkg/validator"
|
|
domainArticle "RenewCMS/internal/domain/article"
|
|
domainImage "RenewCMS/internal/domain/image"
|
|
"RenewCMS/internal/infrastructure/useCases"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/jwtauth/v5"
|
|
)
|
|
|
|
func seedArticleImages(imageRepo *mockImageRepo, ids ...uint32) []*domainImage.Image {
|
|
images := make([]*domainImage.Image, len(ids))
|
|
for i, id := range ids {
|
|
imageRepo.images[id] = domainImage.Image{ID: id}
|
|
seeded := imageRepo.images[id]
|
|
images[i] = &seeded
|
|
}
|
|
return images
|
|
}
|
|
|
|
func TestDeleteArticleUseCaseCascadesImageDeletion(t *testing.T) {
|
|
t.Run("deletes every image before deleting the article, in Images slice order", func(t *testing.T) {
|
|
articleRepo := newMockArticleRepo()
|
|
imageRepo := newMockImageRepo()
|
|
articleRepo.articles[1] = domainArticle.Article{ID: 1, Images: seedArticleImages(imageRepo, 10, 11)}
|
|
uc := useCases.NewDeleteArticleUseCase(articleRepo, imageRepo)
|
|
|
|
if err := uc.DeleteArticle(1); err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if got := imageRepo.deleteCalls; len(got) != 2 || got[0] != 10 || got[1] != 11 {
|
|
t.Errorf("expected image Delete to be called with [10 11] in order, got %v", got)
|
|
}
|
|
if _, exists := articleRepo.articles[1]; exists {
|
|
t.Errorf("expected article 1 to have been deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("stops at the first image Delete failure and leaves the article untouched", func(t *testing.T) {
|
|
articleRepo := newMockArticleRepo()
|
|
imageRepo := newMockImageRepo()
|
|
articleRepo.articles[3] = domainArticle.Article{ID: 3, Images: seedArticleImages(imageRepo, 20, 21)}
|
|
imageRepo.failDeleteFor = map[uint32]error{20: errRepositoryFailure}
|
|
uc := useCases.NewDeleteArticleUseCase(articleRepo, imageRepo)
|
|
|
|
err := uc.DeleteArticle(3)
|
|
if !errors.Is(err, errRepositoryFailure) {
|
|
t.Fatalf("expected errRepositoryFailure, got %v", err)
|
|
}
|
|
if got := imageRepo.deleteCalls; len(got) != 1 || got[0] != 20 {
|
|
t.Errorf("expected image Delete to be called only once with 20, got %v", got)
|
|
}
|
|
if _, exists := articleRepo.articles[3]; !exists {
|
|
t.Errorf("expected article 3 to still be present")
|
|
}
|
|
if _, exists := imageRepo.images[21]; !exists {
|
|
t.Errorf("expected image 21 to still be present")
|
|
}
|
|
})
|
|
|
|
t.Run("propagates ErrArticleNotFound without touching the image repository", func(t *testing.T) {
|
|
articleRepo := newMockArticleRepo()
|
|
imageRepo := newMockImageRepo()
|
|
uc := useCases.NewDeleteArticleUseCase(articleRepo, imageRepo)
|
|
|
|
err := uc.DeleteArticle(9999)
|
|
if !errors.Is(err, domainArticle.ErrArticleNotFound) {
|
|
t.Fatalf("expected article.ErrArticleNotFound, got %v", err)
|
|
}
|
|
if len(imageRepo.deleteCalls) != 0 {
|
|
t.Errorf("expected no call to the image repository's Delete method, got %v", imageRepo.deleteCalls)
|
|
}
|
|
})
|
|
}
|
|
|
|
func newDeleteArticleCascadesImagesRouter(articleRepo *mockArticleRepo, imageRepo *mockImageRepo, token *jwtauth.JWTAuth) *chi.Mux {
|
|
userRepo := newVerifiedUserRepo()
|
|
getUserUseCase := useCases.NewGetUserUseCase(userRepo)
|
|
|
|
articleHandler := article.NewHandler(
|
|
useCases.NewCreateArticleUseCase(articleRepo),
|
|
useCases.NewGetArticleUseCase(articleRepo),
|
|
useCases.NewListArticlesUseCase(articleRepo),
|
|
useCases.NewDeleteArticleUseCase(articleRepo, imageRepo),
|
|
useCases.NewUpdateArticleUseCase(articleRepo),
|
|
getUserUseCase,
|
|
)
|
|
|
|
r := chi.NewRouter()
|
|
r.Route("/v1", func(r chi.Router) {
|
|
r.Group(func(r chi.Router) {
|
|
r.Use(jwtauth.Verifier(token))
|
|
r.Use(jwtauth.Authenticator(token))
|
|
r.Use(middleware.RequireVerifiedUser(getUserUseCase))
|
|
r.Route("/article", func(r chi.Router) {
|
|
r.Delete("/{id}", articleHandler.DeleteArticle)
|
|
})
|
|
})
|
|
})
|
|
return r
|
|
}
|
|
|
|
func TestDeleteArticleCascadesImages(t *testing.T) {
|
|
validator.InitValidator()
|
|
token := jwtauth.New("HS256", []byte("test-secret"), nil)
|
|
|
|
t.Run("Deleting an article with images removes the article and every one of its images", func(t *testing.T) {
|
|
articleRepo := newMockArticleRepo()
|
|
imageRepo := newMockImageRepo()
|
|
articleRepo.articles[1] = domainArticle.Article{ID: 1, Images: seedArticleImages(imageRepo, 10, 11)}
|
|
|
|
router := newDeleteArticleCascadesImagesRouter(articleRepo, imageRepo, token)
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/article/1", nil)
|
|
setJwtCookie(t, req, token, 1)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("expected status 204, got %d", rec.Code)
|
|
}
|
|
if _, exists := articleRepo.articles[1]; exists {
|
|
t.Errorf("expected article 1 to have been deleted")
|
|
}
|
|
if _, exists := imageRepo.images[10]; exists {
|
|
t.Errorf("expected image 10 to have been deleted")
|
|
}
|
|
if _, exists := imageRepo.images[11]; exists {
|
|
t.Errorf("expected image 11 to have been deleted")
|
|
}
|
|
})
|
|
|
|
t.Run("Deleting an article with no images still succeeds", func(t *testing.T) {
|
|
articleRepo := newMockArticleRepo()
|
|
imageRepo := newMockImageRepo()
|
|
articleRepo.articles[2] = domainArticle.Article{ID: 2}
|
|
|
|
router := newDeleteArticleCascadesImagesRouter(articleRepo, imageRepo, token)
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/article/2", nil)
|
|
setJwtCookie(t, req, token, 1)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("expected status 204, got %d", rec.Code)
|
|
}
|
|
if _, exists := articleRepo.articles[2]; exists {
|
|
t.Errorf("expected article 2 to have been deleted")
|
|
}
|
|
if len(imageRepo.deleteCalls) != 0 {
|
|
t.Errorf("expected no call to the image repository's Delete method, got %v", imageRepo.deleteCalls)
|
|
}
|
|
})
|
|
|
|
t.Run("Deleting an unknown article still answers 404 and deletes no images", func(t *testing.T) {
|
|
articleRepo := newMockArticleRepo()
|
|
imageRepo := newMockImageRepo()
|
|
|
|
router := newDeleteArticleCascadesImagesRouter(articleRepo, imageRepo, token)
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/article/9999", nil)
|
|
setJwtCookie(t, req, token, 1)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("expected status 404, got %d", rec.Code)
|
|
}
|
|
if rec.Body.String() != apperr.ArticleNotFoundErrorMessage+"\n" {
|
|
t.Errorf("unexpected body: %q", rec.Body.String())
|
|
}
|
|
if len(imageRepo.deleteCalls) != 0 {
|
|
t.Errorf("expected no call to the image repository's Delete method, got %v", imageRepo.deleteCalls)
|
|
}
|
|
})
|
|
|
|
t.Run("Deleting an article still requires authentication", func(t *testing.T) {
|
|
articleRepo := newMockArticleRepo()
|
|
imageRepo := newMockImageRepo()
|
|
articleRepo.articles[1] = domainArticle.Article{ID: 1, Images: seedArticleImages(imageRepo, 10)}
|
|
|
|
router := newDeleteArticleCascadesImagesRouter(articleRepo, imageRepo, token)
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/article/1", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected status 401, got %d", rec.Code)
|
|
}
|
|
if _, exists := articleRepo.articles[1]; !exists {
|
|
t.Errorf("expected article 1 to still be present")
|
|
}
|
|
if _, exists := imageRepo.images[10]; !exists {
|
|
t.Errorf("expected image 10 to still be present")
|
|
}
|
|
})
|
|
|
|
t.Run("A failure deleting one of an article's images aborts the whole delete", func(t *testing.T) {
|
|
articleRepo := newMockArticleRepo()
|
|
imageRepo := newMockImageRepo()
|
|
articleRepo.articles[3] = domainArticle.Article{ID: 3, Images: seedArticleImages(imageRepo, 20, 21)}
|
|
imageRepo.failDeleteFor = map[uint32]error{20: errRepositoryFailure}
|
|
|
|
router := newDeleteArticleCascadesImagesRouter(articleRepo, imageRepo, token)
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/article/3", nil)
|
|
setJwtCookie(t, req, token, 1)
|
|
rec := httptest.NewRecorder()
|
|
router.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("expected status 500, got %d", rec.Code)
|
|
}
|
|
if rec.Body.String() != "Failed to delete article\n" {
|
|
t.Errorf("unexpected body: %q", rec.Body.String())
|
|
}
|
|
if _, exists := articleRepo.articles[3]; !exists {
|
|
t.Errorf("expected article 3 to still be present")
|
|
}
|
|
if _, exists := imageRepo.images[21]; !exists {
|
|
t.Errorf("expected image 21 to still be present")
|
|
}
|
|
})
|
|
}
|