To gitea and beyond, let's go(-yco)
This commit is contained in:
711
internal/handlers/post_handler_test.go
Normal file
711
internal/handlers/post_handler_test.go
Normal file
@@ -0,0 +1,711 @@
|
||||
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) 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user