package handler import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "tss-rocks-be/ent" "tss-rocks-be/internal/config" "tss-rocks-be/internal/service" "tss-rocks-be/internal/service/mock" "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" "go.uber.org/mock/gomock" "errors" "strings" ) type PostHandlerTestSuite struct { suite.Suite ctrl *gomock.Controller service *mock.MockService handler *Handler router *gin.Engine } func (s *PostHandlerTestSuite) SetupTest() { s.ctrl = gomock.NewController(s.T()) s.service = mock.NewMockService(s.ctrl) cfg := &config.Config{ JWT: config.JWTConfig{ Secret: "test-secret", }, } s.handler = NewHandler(cfg, s.service) // Setup Gin router gin.SetMode(gin.TestMode) s.router = gin.New() // Setup mock for GetTokenBlacklist tokenBlacklist := &service.TokenBlacklist{} s.service.EXPECT(). GetTokenBlacklist(). Return(tokenBlacklist). AnyTimes() s.handler.RegisterRoutes(s.router) } func (s *PostHandlerTestSuite) TearDownTest() { s.ctrl.Finish() } func TestPostHandlerSuite(t *testing.T) { suite.Run(t, new(PostHandlerTestSuite)) } // Test cases for ListPosts func (s *PostHandlerTestSuite) TestListPosts() { categoryID := 1 testCases := []struct { name string langCode string categoryID string limit string offset string setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success with default language", langCode: "", setupMock: func() { s.service.EXPECT(). ListPosts(gomock.Any(), "en", nil, 10, 0). Return([]*ent.Post{ { ID: 1, Status: "published", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", }, }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []*ent.Post{ { ID: 1, Status: "published", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", }, }, }, }, }, }, { name: "Success with specific language", langCode: "zh", setupMock: func() { s.service.EXPECT(). ListPosts(gomock.Any(), "zh", nil, 10, 0). Return([]*ent.Post{ { ID: 1, Status: "published", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "zh", Title: "测试帖子", ContentMarkdown: "测试内容", Summary: "测试摘要", }, }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []*ent.Post{ { ID: 1, Status: "published", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "zh", Title: "测试帖子", ContentMarkdown: "测试内容", Summary: "测试摘要", }, }, }, }, }, }, { name: "Success with category filter", langCode: "en", categoryID: "1", setupMock: func() { s.service.EXPECT(). ListPosts(gomock.Any(), "en", &categoryID, 10, 0). Return([]*ent.Post{ { ID: 1, Status: "published", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", }, }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []*ent.Post{ { ID: 1, Status: "published", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", }, }, }, }, }, }, { name: "Success with pagination", langCode: "en", limit: "2", offset: "1", setupMock: func() { s.service.EXPECT(). ListPosts(gomock.Any(), "en", nil, 2, 1). Return([]*ent.Post{ { ID: 2, Status: "published", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "en", Title: "Test Post 2", ContentMarkdown: "Test Content 2", Summary: "Test Summary 2", }, }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []*ent.Post{ { ID: 2, Status: "published", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "en", Title: "Test Post 2", ContentMarkdown: "Test Content 2", Summary: "Test Summary 2", }, }, }, }, }, }, { name: "Service Error", langCode: "en", setupMock: func() { s.service.EXPECT(). ListPosts(gomock.Any(), "en", nil, 10, 0). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, }, } for _, tc := range testCases { s.Run(tc.name, func() { // Setup mock tc.setupMock() // Create request url := "/api/v1/posts" if tc.langCode != "" { url += "?lang=" + tc.langCode } if tc.categoryID != "" { if strings.Contains(url, "?") { url += "&" } else { url += "?" } url += "category_id=" + tc.categoryID } if tc.limit != "" { if strings.Contains(url, "?") { url += "&" } else { url += "?" } url += "limit=" + tc.limit } if tc.offset != "" { if strings.Contains(url, "?") { url += "&" } else { url += "?" } url += "offset=" + tc.offset } req := httptest.NewRequest(http.MethodGet, url, nil) w := httptest.NewRecorder() // Perform request s.router.ServeHTTP(w, req) // Assert response s.Equal(tc.expectedStatus, w.Code) if tc.expectedBody != nil { var response []*ent.Post err := json.Unmarshal(w.Body.Bytes(), &response) s.NoError(err) s.Equal(tc.expectedBody, response) } }) } } // Test cases for GetPost func (s *PostHandlerTestSuite) TestGetPost() { testCases := []struct { name string langCode string slug string setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success with default language", langCode: "", slug: "test-post", setupMock: func() { s.service.EXPECT(). GetPostBySlug(gomock.Any(), "en", "test-post"). Return(&ent.Post{ ID: 1, Status: "published", Slug: "test-post", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: gin.H{ "id": 1, "status": "published", "slug": "test-post", "edges": gin.H{ "contents": []gin.H{ { "language_code": "en", "title": "Test Post", "content_markdown": "Test Content", "summary": "Test Summary", }, }, }, }, }, { name: "Success with specific language", langCode: "zh", slug: "test-post", setupMock: func() { s.service.EXPECT(). GetPostBySlug(gomock.Any(), "zh", "test-post"). Return(&ent.Post{ ID: 1, Status: "published", Slug: "test-post", Edges: ent.PostEdges{ Contents: []*ent.PostContent{ { LanguageCode: "zh", Title: "测试帖子", ContentMarkdown: "测试内容", Summary: "测试摘要", }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: gin.H{ "id": 1, "status": "published", "slug": "test-post", "edges": gin.H{ "contents": []gin.H{ { "language_code": "zh", "title": "测试帖子", "content_markdown": "测试内容", "summary": "测试摘要", }, }, }, }, }, { name: "Service error", slug: "test-post", setupMock: func() { s.service.EXPECT(). GetPostBySlug(gomock.Any(), "en", "test-post"). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to get post"}, }, } for _, tc := range testCases { s.Run(tc.name, func() { tc.setupMock() url := "/api/v1/posts/" + tc.slug if tc.langCode != "" { url += "?lang=" + tc.langCode } req := httptest.NewRequest(http.MethodGet, url, nil) w := httptest.NewRecorder() s.router.ServeHTTP(w, req) s.Equal(tc.expectedStatus, w.Code, "HTTP status code mismatch") if tc.expectedBody != nil { expectedJSON, err := json.Marshal(tc.expectedBody) s.NoError(err, "Failed to marshal expected body") s.JSONEq(string(expectedJSON), w.Body.String(), "Response body mismatch") } }) } } // Test cases for CreatePost func (s *PostHandlerTestSuite) TestCreatePost() { testCases := []struct { name string setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", setupMock: func() { s.service.EXPECT(). CreatePost(gomock.Any(), "draft"). Return(&ent.Post{ ID: 1, Status: "draft", Edges: ent.PostEdges{ Contents: []*ent.PostContent{}, }, }, nil) }, expectedStatus: http.StatusCreated, expectedBody: gin.H{ "id": 1, "status": "draft", "edges": gin.H{ "contents": []gin.H{}, }, }, }, { name: "Service error", setupMock: func() { s.service.EXPECT(). CreatePost(gomock.Any(), "draft"). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to create post"}, }, } for _, tc := range testCases { s.Run(tc.name, func() { tc.setupMock() req := httptest.NewRequest(http.MethodPost, "/api/v1/posts", nil) w := httptest.NewRecorder() s.router.ServeHTTP(w, req) s.Equal(tc.expectedStatus, w.Code, "HTTP status code mismatch") if tc.expectedBody != nil { expectedJSON, err := json.Marshal(tc.expectedBody) s.NoError(err, "Failed to marshal expected body") s.JSONEq(string(expectedJSON), w.Body.String(), "Response body mismatch") } }) } } // Test cases for AddPostContent func (s *PostHandlerTestSuite) TestAddPostContent() { testCases := []struct { name string postID string body interface{} setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", postID: "1", body: AddPostContentRequest{ LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", MetaKeywords: "test,keywords", MetaDescription: "Test meta description", }, setupMock: func() { s.service.EXPECT(). AddPostContent( gomock.Any(), 1, "en", "Test Post", "Test Content", "Test Summary", "test,keywords", "Test meta description", ). Return(&ent.PostContent{ LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", MetaKeywords: "test,keywords", MetaDescription: "Test meta description", Edges: ent.PostContentEdges{}, }, nil) }, expectedStatus: http.StatusCreated, expectedBody: gin.H{ "language_code": "en", "title": "Test Post", "content_markdown": "Test Content", "summary": "Test Summary", "meta_keywords": "test,keywords", "meta_description": "Test meta description", "edges": gin.H{}, }, }, { name: "Invalid post ID", postID: "invalid", body: AddPostContentRequest{ LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", }, setupMock: func() {}, expectedStatus: http.StatusBadRequest, expectedBody: gin.H{"error": "Invalid post ID"}, }, { name: "Invalid request body", postID: "1", body: map[string]interface{}{ "language_code": "en", // Missing required fields }, setupMock: func() {}, expectedStatus: http.StatusBadRequest, expectedBody: gin.H{"error": "Key: 'AddPostContentRequest.Title' Error:Field validation for 'Title' failed on the 'required' tag\nKey: 'AddPostContentRequest.ContentMarkdown' Error:Field validation for 'ContentMarkdown' failed on the 'required' tag\nKey: 'AddPostContentRequest.Summary' Error:Field validation for 'Summary' failed on the 'required' tag"}, }, { name: "Service error", postID: "1", body: AddPostContentRequest{ LanguageCode: "en", Title: "Test Post", ContentMarkdown: "Test Content", Summary: "Test Summary", MetaKeywords: "test,keywords", MetaDescription: "Test meta description", }, setupMock: func() { s.service.EXPECT(). AddPostContent( gomock.Any(), 1, "en", "Test Post", "Test Content", "Test Summary", "test,keywords", "Test meta description", ). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to add post content"}, }, } for _, tc := range testCases { s.Run(tc.name, func() { tc.setupMock() body, err := json.Marshal(tc.body) s.NoError(err, "Failed to marshal request body") req := httptest.NewRequest(http.MethodPost, "/api/v1/posts/"+tc.postID+"/contents", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() s.router.ServeHTTP(w, req) s.Equal(tc.expectedStatus, w.Code, "HTTP status code mismatch") if tc.expectedBody != nil { expectedJSON, err := json.Marshal(tc.expectedBody) s.NoError(err, "Failed to marshal expected body") s.JSONEq(string(expectedJSON), w.Body.String(), "Response body mismatch") } }) } }