715 lines
24 KiB
Go
715 lines
24 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/jackc/pgconn"
|
|
"gorm.io/gorm"
|
|
"goyco/internal/database"
|
|
"goyco/internal/middleware"
|
|
"goyco/internal/repositories"
|
|
"goyco/internal/services"
|
|
"goyco/internal/testutils"
|
|
)
|
|
|
|
func decodeHandlerResponse(t *testing.T, rr *httptest.ResponseRecorder) map[string]any {
|
|
t.Helper()
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func TestPostHandlerGetPostsWithVoteService(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
repo.GetAllFn = func(limit, offset int) ([]database.Post, error) {
|
|
return []database.Post{
|
|
{ID: 1, Title: "Test Post 1"},
|
|
{ID: 2, Title: "Test Post 2"},
|
|
}, nil
|
|
}
|
|
|
|
voteRepo := testutils.NewMockVoteRepository()
|
|
voteService := services.NewVoteService(voteRepo, repo, nil)
|
|
handler := NewPostHandler(repo, nil, voteService)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/api/posts", nil)
|
|
request = testutils.WithUserContext(request, middleware.UserIDKey, uint(1))
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.GetPosts(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusOK)
|
|
|
|
payload := decodeHandlerResponse(t, recorder)
|
|
if !payload["success"].(bool) {
|
|
t.Fatalf("expected success response, got %v", payload)
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerCreatePostWithTitleFetcher(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
var storedPost *database.Post
|
|
repo.CreateFn = func(post *database.Post) error {
|
|
storedPost = post
|
|
return nil
|
|
}
|
|
|
|
titleFetcher := &testutils.MockTitleFetcher{}
|
|
titleFetcher.SetTitle("Fetched Title")
|
|
|
|
handler := NewPostHandler(repo, titleFetcher, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/api/posts", bytes.NewBufferString(`{"url":"https://example.com","content":"Test content"}`))
|
|
request = testutils.WithUserContext(request, middleware.UserIDKey, uint(1))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.CreatePost(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusCreated)
|
|
|
|
if storedPost == nil {
|
|
t.Fatal("expected post to be created")
|
|
}
|
|
|
|
if storedPost.Title != "Fetched Title" {
|
|
t.Errorf("expected title 'Fetched Title', got %s", storedPost.Title)
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerCreatePostTitleFetcherError(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
titleFetcher := &testutils.MockTitleFetcher{}
|
|
titleFetcher.SetError(services.ErrUnsupportedScheme)
|
|
|
|
handler := NewPostHandler(repo, titleFetcher, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/api/posts", bytes.NewBufferString(`{"url":"ftp://example.com"}`))
|
|
request = testutils.WithUserContext(request, middleware.UserIDKey, uint(1))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.CreatePost(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusBadRequest)
|
|
|
|
payload := decodeHandlerResponse(t, recorder)
|
|
if payload["success"].(bool) {
|
|
t.Fatalf("expected error response, got %v", payload)
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerSearchPosts(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
repo.SearchFn = func(query string, limit, offset int) ([]database.Post, error) {
|
|
return []database.Post{
|
|
{ID: 1, Title: "Search Result 1"},
|
|
{ID: 2, Title: "Search Result 2"},
|
|
}, nil
|
|
}
|
|
|
|
handler := NewPostHandler(repo, nil, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/api/posts/search?q=test", nil)
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.SearchPosts(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusOK)
|
|
|
|
payload := decodeHandlerResponse(t, recorder)
|
|
if !payload["success"].(bool) {
|
|
t.Fatalf("expected success response, got %v", payload)
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerFetchTitleFromURL(t *testing.T) {
|
|
titleFetcher := &testutils.MockTitleFetcher{}
|
|
titleFetcher.SetTitle("Test Title")
|
|
|
|
handler := NewPostHandler(nil, titleFetcher, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/api/posts/title?url=https://example.com", nil)
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.FetchTitleFromURL(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusOK)
|
|
|
|
payload := decodeHandlerResponse(t, recorder)
|
|
if !payload["success"].(bool) {
|
|
t.Fatalf("expected success response, got %v", payload)
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerFetchTitleFromURLNoFetcher(t *testing.T) {
|
|
handler := NewPostHandler(nil, nil, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/api/posts/title?url=https://example.com", nil)
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.FetchTitleFromURL(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusNotImplemented)
|
|
}
|
|
|
|
func TestPostHandlerUpdatePostUnauthorized(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
return &database.Post{ID: id, AuthorID: func() *uint { u := uint(2); return &u }()}, nil
|
|
}
|
|
|
|
handler := NewPostHandler(repo, nil, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodPut, "/api/posts/1", bytes.NewBufferString(`{"title":"Updated Title","content":"Updated content"}`))
|
|
request = testutils.WithUserContext(request, middleware.UserIDKey, uint(1))
|
|
request = testutils.WithURLParams(request, map[string]string{"id": "1"})
|
|
request.Header.Set("Content-Type", "application/json")
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.UpdatePost(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusForbidden)
|
|
}
|
|
|
|
func TestPostHandlerDeletePostUnauthorized(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
return &database.Post{ID: id, AuthorID: func() *uint { u := uint(2); return &u }()}, nil
|
|
}
|
|
|
|
voteRepo := testutils.NewMockVoteRepository()
|
|
voteService := services.NewVoteService(voteRepo, repo, nil)
|
|
handler := NewPostHandler(repo, nil, voteService)
|
|
|
|
request := httptest.NewRequest(http.MethodDelete, "/api/posts/1", nil)
|
|
request = testutils.WithUserContext(request, middleware.UserIDKey, uint(1))
|
|
request = testutils.WithURLParams(request, map[string]string{"id": "1"})
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.DeletePost(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusForbidden)
|
|
}
|
|
|
|
func TestPostHandlerGetPosts(t *testing.T) {
|
|
var receivedLimit, receivedOffset int
|
|
repo := testutils.NewPostRepositoryStub()
|
|
repo.GetAllFn = func(limit, offset int) ([]database.Post, error) {
|
|
receivedLimit = limit
|
|
receivedOffset = offset
|
|
return []database.Post{{ID: 1}}, nil
|
|
}
|
|
|
|
handler := NewPostHandler(repo, nil, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/api/posts?limit=5&offset=2", nil)
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.GetPosts(recorder, request)
|
|
|
|
if receivedLimit != 5 || receivedOffset != 2 {
|
|
t.Fatalf("expected limit=5 offset=2, got %d %d", receivedLimit, receivedOffset)
|
|
}
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusOK)
|
|
|
|
payload := decodeHandlerResponse(t, recorder)
|
|
if !payload["success"].(bool) {
|
|
t.Fatalf("expected success response, got %v", payload)
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerGetPostErrors(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
handler := NewPostHandler(repo, nil, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/api/posts", nil)
|
|
recorder := httptest.NewRecorder()
|
|
handler.GetPost(recorder, request)
|
|
if recorder.Result().StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for missing id, got %d", recorder.Result().StatusCode)
|
|
}
|
|
|
|
request = httptest.NewRequest(http.MethodGet, "/api/posts/abc", nil)
|
|
request = testutils.WithURLParams(request, map[string]string{"id": "abc"})
|
|
recorder = httptest.NewRecorder()
|
|
handler.GetPost(recorder, request)
|
|
if recorder.Result().StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid id, got %d", recorder.Result().StatusCode)
|
|
}
|
|
|
|
repo.GetByIDFn = func(uint) (*database.Post, error) {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
request = httptest.NewRequest(http.MethodGet, "/api/posts/1", nil)
|
|
request = testutils.WithURLParams(request, map[string]string{"id": "1"})
|
|
recorder = httptest.NewRecorder()
|
|
handler.GetPost(recorder, request)
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusNotFound)
|
|
}
|
|
|
|
func TestPostHandlerCreatePostSuccess(t *testing.T) {
|
|
var storedPost *database.Post
|
|
repo := testutils.NewPostRepositoryStub()
|
|
repo.CreateFn = func(post *database.Post) error {
|
|
storedPost = &database.Post{
|
|
Title: post.Title,
|
|
URL: post.URL,
|
|
Content: post.Content,
|
|
AuthorID: post.AuthorID,
|
|
}
|
|
storedPost.ID = 1
|
|
return nil
|
|
}
|
|
fetcher := &testutils.TitleFetcherStub{FetchTitleFn: func(ctx context.Context, rawURL string) (string, error) {
|
|
return "Fetched Title", nil
|
|
}}
|
|
|
|
handler := NewPostHandler(repo, fetcher, nil)
|
|
|
|
body := bytes.NewBufferString(`{"title":" ","url":"https://example.com","content":"Go"}`)
|
|
request := httptest.NewRequest(http.MethodPost, "/api/posts", body)
|
|
ctx := context.WithValue(request.Context(), middleware.UserIDKey, uint(42))
|
|
request = request.WithContext(ctx)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
handler.CreatePost(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusCreated)
|
|
|
|
if storedPost == nil || storedPost.Title != "Fetched Title" || storedPost.AuthorID == nil || *storedPost.AuthorID != 42 {
|
|
t.Fatalf("unexpected stored post: %#v", storedPost)
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerCreatePostValidation(t *testing.T) {
|
|
handler := NewPostHandler(testutils.NewPostRepositoryStub(), &testutils.TitleFetcherStub{}, nil)
|
|
|
|
recorder := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodPost, "/api/posts", bytes.NewBufferString(`{"title":"","url":"","content":""}`))
|
|
request = request.WithContext(context.WithValue(request.Context(), middleware.UserIDKey, uint(1)))
|
|
handler.CreatePost(recorder, request)
|
|
if recorder.Result().StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for missing url, got %d", recorder.Result().StatusCode)
|
|
}
|
|
|
|
recorder = httptest.NewRecorder()
|
|
request = httptest.NewRequest(http.MethodPost, "/api/posts", bytes.NewBufferString(`invalid json`))
|
|
handler.CreatePost(recorder, request)
|
|
if recorder.Result().StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid JSON, got %d", recorder.Result().StatusCode)
|
|
}
|
|
|
|
recorder = httptest.NewRecorder()
|
|
request = httptest.NewRequest(http.MethodPost, "/api/posts", bytes.NewBufferString(`{"title":"ok","url":"https://example.com"}`))
|
|
handler.CreatePost(recorder, request)
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusUnauthorized)
|
|
}
|
|
|
|
func TestPostHandlerCreatePostTitleFetcherErrors(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
wantStatus int
|
|
wantMsg string
|
|
}{
|
|
{name: "Unsupported", err: services.ErrUnsupportedScheme, wantStatus: http.StatusBadRequest, wantMsg: "Only HTTP and HTTPS URLs are supported"},
|
|
{name: "TitleMissing", err: services.ErrTitleNotFound, wantStatus: http.StatusBadRequest, wantMsg: "Title could not be extracted"},
|
|
{name: "Generic", err: errors.New("timeout"), wantStatus: http.StatusBadGateway, wantMsg: "Failed to fetch title"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
fetcher := &testutils.TitleFetcherStub{FetchTitleFn: func(ctx context.Context, rawURL string) (string, error) {
|
|
return "", tc.err
|
|
}}
|
|
handler := NewPostHandler(repo, fetcher, nil)
|
|
body := bytes.NewBufferString(`{"title":" ","url":"https://example.com"}`)
|
|
request := httptest.NewRequest(http.MethodPost, "/api/posts", body)
|
|
request = request.WithContext(context.WithValue(request.Context(), middleware.UserIDKey, uint(1)))
|
|
|
|
recorder := httptest.NewRecorder()
|
|
handler.CreatePost(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, tc.wantStatus)
|
|
|
|
if !strings.Contains(recorder.Body.String(), tc.wantMsg) {
|
|
t.Fatalf("expected message to contain %q, got %q", tc.wantMsg, recorder.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerFetchTitleFromURLErrors(t *testing.T) {
|
|
handler := NewPostHandler(testutils.NewPostRepositoryStub(), nil, nil)
|
|
request := httptest.NewRequest(http.MethodGet, "/api/posts/title?url=https://example.com", nil)
|
|
recorder := httptest.NewRecorder()
|
|
handler.FetchTitleFromURL(recorder, request)
|
|
if recorder.Result().StatusCode != http.StatusNotImplemented {
|
|
t.Fatalf("expected 501 when fetcher unavailable, got %d", recorder.Result().StatusCode)
|
|
}
|
|
|
|
handler = NewPostHandler(testutils.NewPostRepositoryStub(), &testutils.TitleFetcherStub{}, nil)
|
|
request = httptest.NewRequest(http.MethodGet, "/api/posts/title", nil)
|
|
recorder = httptest.NewRecorder()
|
|
handler.FetchTitleFromURL(recorder, request)
|
|
if recorder.Result().StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for missing url query, got %d", recorder.Result().StatusCode)
|
|
}
|
|
|
|
handler = NewPostHandler(testutils.NewPostRepositoryStub(), &testutils.TitleFetcherStub{FetchTitleFn: func(ctx context.Context, rawURL string) (string, error) {
|
|
return "", errors.New("failed")
|
|
}}, nil)
|
|
request = httptest.NewRequest(http.MethodGet, "/api/posts/title?url=https://example.com", nil)
|
|
recorder = httptest.NewRecorder()
|
|
handler.FetchTitleFromURL(recorder, request)
|
|
testutils.AssertHTTPStatus(t, recorder, http.StatusBadGateway)
|
|
}
|
|
|
|
func TestTranslatePostCreateError(t *testing.T) {
|
|
conflictErr := &pgconn.PgError{Code: "23505"}
|
|
msg, status := translatePostCreateError(conflictErr)
|
|
if status != http.StatusConflict || !strings.Contains(msg, "already been submitted") {
|
|
t.Fatalf("unexpected conflict translation: status=%d msg=%q", status, msg)
|
|
}
|
|
|
|
fkErr := &pgconn.PgError{Code: "23503"}
|
|
msg, status = translatePostCreateError(fkErr)
|
|
if status != http.StatusUnauthorized || !strings.Contains(msg, "Author account not found") {
|
|
t.Fatalf("unexpected foreign key translation: status=%d msg=%q", status, msg)
|
|
}
|
|
|
|
msg, status = translatePostCreateError(errors.New("other"))
|
|
if status != 0 || msg != "" {
|
|
t.Fatalf("expected passthrough for unrelated errors, got status=%d msg=%q", status, msg)
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerUpdatePost(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
postID string
|
|
requestBody string
|
|
userID uint
|
|
mockSetup func(*testutils.PostRepositoryStub)
|
|
expectedStatus int
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "valid post update",
|
|
postID: "1",
|
|
requestBody: `{"title": "Updated Title", "content": "Updated content"}`,
|
|
userID: 1,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
authorID := uint(1)
|
|
return &database.Post{ID: id, Title: "Old Title", AuthorID: &authorID}, nil
|
|
}
|
|
repo.UpdateFn = func(post *database.Post) error { return nil }
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "missing user context",
|
|
postID: "1",
|
|
requestBody: `{"title": "Updated Title", "content": "Updated content"}`,
|
|
userID: 0,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
expectedError: "Authentication required",
|
|
},
|
|
{
|
|
name: "post not found",
|
|
postID: "999",
|
|
requestBody: `{"title": "Updated Title", "content": "Updated content"}`,
|
|
userID: 1,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
},
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedError: "Post not found",
|
|
},
|
|
{
|
|
name: "not author",
|
|
postID: "1",
|
|
requestBody: `{"title": "Updated Title", "content": "Updated content"}`,
|
|
userID: 2,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
authorID := uint(1)
|
|
return &database.Post{ID: id, Title: "Old Title", AuthorID: &authorID}, nil
|
|
}
|
|
},
|
|
expectedStatus: http.StatusForbidden,
|
|
expectedError: "You can only edit your own posts",
|
|
},
|
|
{
|
|
name: "empty title",
|
|
postID: "1",
|
|
requestBody: `{"title": "", "content": "Updated content"}`,
|
|
userID: 1,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
authorID := uint(1)
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
return &database.Post{ID: id, Title: "Old Title", AuthorID: &authorID}, nil
|
|
}
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedError: "Title is required",
|
|
},
|
|
{
|
|
name: "short title",
|
|
postID: "1",
|
|
requestBody: `{"title": "ab", "content": "Updated content"}`,
|
|
userID: 1,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
authorID := uint(1)
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
return &database.Post{ID: id, Title: "Old Title", AuthorID: &authorID}, nil
|
|
}
|
|
},
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedError: "Title must be at least 3 characters",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
if tt.mockSetup != nil {
|
|
tt.mockSetup(repo)
|
|
}
|
|
handler := NewPostHandler(repo, &testutils.TitleFetcherStub{}, nil)
|
|
|
|
request := httptest.NewRequest(http.MethodPut, "/api/posts/"+tt.postID, bytes.NewBufferString(tt.requestBody))
|
|
if tt.userID > 0 {
|
|
ctx := context.WithValue(request.Context(), middleware.UserIDKey, tt.userID)
|
|
request = request.WithContext(ctx)
|
|
}
|
|
|
|
ctx := chi.NewRouteContext()
|
|
ctx.URLParams.Add("id", tt.postID)
|
|
request = request.WithContext(context.WithValue(request.Context(), chi.RouteCtxKey, ctx))
|
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.UpdatePost(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, tt.expectedStatus)
|
|
|
|
if tt.expectedError != "" {
|
|
if !strings.Contains(recorder.Body.String(), tt.expectedError) {
|
|
t.Fatalf("expected error to contain %q, got %q", tt.expectedError, recorder.Body.String())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPostHandlerDeletePost(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
postID string
|
|
userID uint
|
|
mockSetup func(*testutils.PostRepositoryStub)
|
|
expectedStatus int
|
|
expectedError string
|
|
}{
|
|
{
|
|
name: "valid post deletion",
|
|
postID: "1",
|
|
userID: 1,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
authorID := uint(1)
|
|
return &database.Post{ID: id, Title: "Test Post", AuthorID: &authorID}, nil
|
|
}
|
|
repo.DeleteFn = func(id uint) error { return nil }
|
|
},
|
|
expectedStatus: http.StatusOK,
|
|
},
|
|
{
|
|
name: "missing user context",
|
|
postID: "1",
|
|
userID: 0,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {},
|
|
expectedStatus: http.StatusUnauthorized,
|
|
expectedError: "Authentication required",
|
|
},
|
|
{
|
|
name: "post not found",
|
|
postID: "999",
|
|
userID: 1,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
},
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedError: "Post not found",
|
|
},
|
|
{
|
|
name: "not author",
|
|
postID: "1",
|
|
userID: 2,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
authorID := uint(1)
|
|
return &database.Post{ID: id, Title: "Test Post", AuthorID: &authorID}, nil
|
|
}
|
|
},
|
|
expectedStatus: http.StatusForbidden,
|
|
expectedError: "You can only delete your own posts",
|
|
},
|
|
{
|
|
name: "delete error",
|
|
postID: "1",
|
|
userID: 1,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
authorID := uint(1)
|
|
return &database.Post{ID: id, Title: "Test Post", AuthorID: &authorID}, nil
|
|
}
|
|
repo.DeleteFn = func(id uint) error { return errors.New("database error") }
|
|
},
|
|
expectedStatus: http.StatusInternalServerError,
|
|
expectedError: "Failed to delete post",
|
|
},
|
|
{
|
|
name: "delete votes error",
|
|
postID: "1",
|
|
userID: 1,
|
|
mockSetup: func(repo *testutils.PostRepositoryStub) {
|
|
repo.GetByIDFn = func(id uint) (*database.Post, error) {
|
|
authorID := uint(1)
|
|
return &database.Post{ID: id, Title: "Test Post", AuthorID: &authorID}, nil
|
|
}
|
|
},
|
|
expectedStatus: http.StatusInternalServerError,
|
|
expectedError: "Failed to delete post votes",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
repo := testutils.NewPostRepositoryStub()
|
|
if tt.mockSetup != nil {
|
|
tt.mockSetup(repo)
|
|
}
|
|
|
|
var voteService *services.VoteService
|
|
if tt.name == "delete votes error" {
|
|
voteRepo := &errorVoteRepository{}
|
|
voteService = services.NewVoteService(voteRepo, repo, nil)
|
|
} else {
|
|
voteRepo := testutils.NewMockVoteRepository()
|
|
voteService = services.NewVoteService(voteRepo, repo, nil)
|
|
}
|
|
|
|
handler := NewPostHandler(repo, &testutils.TitleFetcherStub{}, voteService)
|
|
|
|
request := httptest.NewRequest(http.MethodDelete, "/api/posts/"+tt.postID, nil)
|
|
if tt.userID > 0 {
|
|
ctx := context.WithValue(request.Context(), middleware.UserIDKey, tt.userID)
|
|
request = request.WithContext(ctx)
|
|
}
|
|
|
|
ctx := chi.NewRouteContext()
|
|
ctx.URLParams.Add("id", tt.postID)
|
|
request = request.WithContext(context.WithValue(request.Context(), chi.RouteCtxKey, ctx))
|
|
|
|
recorder := httptest.NewRecorder()
|
|
|
|
handler.DeletePost(recorder, request)
|
|
|
|
testutils.AssertHTTPStatus(t, recorder, tt.expectedStatus)
|
|
|
|
if tt.expectedError != "" {
|
|
if !strings.Contains(recorder.Body.String(), tt.expectedError) {
|
|
t.Fatalf("expected error to contain %q, got %q", tt.expectedError, recorder.Body.String())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type errorVoteRepository struct{}
|
|
|
|
func (e *errorVoteRepository) Create(*database.Vote) error { return nil }
|
|
func (e *errorVoteRepository) CreateOrUpdate(*database.Vote) error { return nil }
|
|
func (e *errorVoteRepository) GetByID(uint) (*database.Vote, error) {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
func (e *errorVoteRepository) GetByUserAndPost(uint, uint) (*database.Vote, error) {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
func (e *errorVoteRepository) GetByVoteHash(string) (*database.Vote, error) {
|
|
return nil, gorm.ErrRecordNotFound
|
|
}
|
|
func (e *errorVoteRepository) GetByPostID(uint) ([]database.Vote, error) {
|
|
return nil, errors.New("database error")
|
|
}
|
|
func (e *errorVoteRepository) GetByUserID(uint) ([]database.Vote, error) { return nil, nil }
|
|
func (e *errorVoteRepository) Update(*database.Vote) error { return nil }
|
|
func (e *errorVoteRepository) Delete(uint) error { return nil }
|
|
func (e *errorVoteRepository) Count() (int64, error) { return 0, nil }
|
|
func (e *errorVoteRepository) CountByPostID(uint) (int64, error) { return 0, nil }
|
|
func (e *errorVoteRepository) CountByUserID(uint) (int64, error) { return 0, nil }
|
|
func (e *errorVoteRepository) GetVoteCountsByPostID(uint) (int, int, error) {
|
|
return 0, 0, errors.New("database error")
|
|
}
|
|
func (e *errorVoteRepository) WithTx(*gorm.DB) repositories.VoteRepository { return e }
|
|
|
|
func TestPostHandler_EdgeCases(t *testing.T) {
|
|
postRepo := testutils.NewPostRepositoryStub()
|
|
titleFetcher := &testutils.TitleFetcherStub{}
|
|
handler := NewPostHandler(postRepo, titleFetcher, nil)
|
|
|
|
t.Run("GetPosts with zero limit", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/posts?limit=0", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.GetPosts(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200 for zero limit, got %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("GetPosts with negative limit", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/posts?limit=-1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.GetPosts(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200 for negative limit, got %d", w.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("GetPosts with negative offset", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/posts?offset=-1", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
handler.GetPosts(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("Expected status 200 for negative offset, got %d", w.Code)
|
|
}
|
|
})
|
|
}
|