package handler import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "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" ) type ContributorHandlerTestSuite struct { suite.Suite ctrl *gomock.Controller service *mock.MockService handler *Handler router *gin.Engine } func (s *ContributorHandlerTestSuite) 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 *ContributorHandlerTestSuite) TearDownTest() { s.ctrl.Finish() } func TestContributorHandlerSuite(t *testing.T) { suite.Run(t, new(ContributorHandlerTestSuite)) } func (s *ContributorHandlerTestSuite) TestListContributors() { testCases := []struct { name string setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", setupMock: func() { s.service.EXPECT(). ListContributors(gomock.Any()). Return([]*ent.Contributor{ { ID: 1, Name: "John Doe", Edges: ent.ContributorEdges{ SocialLinks: []*ent.ContributorSocialLink{ { Type: "github", Value: "https://github.com/johndoe", Edges: ent.ContributorSocialLinkEdges{}, }, }, }, CreatedAt: time.Time{}, UpdatedAt: time.Time{}, }, { ID: 2, Name: "Jane Smith", Edges: ent.ContributorEdges{ SocialLinks: []*ent.ContributorSocialLink{}, // Ensure empty SocialLinks array is present }, CreatedAt: time.Time{}, UpdatedAt: time.Time{}, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []gin.H{ { "id": 1, "name": "John Doe", "created_at": time.Time{}, "updated_at": time.Time{}, "edges": gin.H{ "social_links": []gin.H{ { "type": "github", "value": "https://github.com/johndoe", "edges": gin.H{}, }, }, }, }, { "id": 2, "name": "Jane Smith", "created_at": time.Time{}, "updated_at": time.Time{}, "edges": gin.H{ "social_links": []gin.H{}, // Ensure empty SocialLinks array is present }, }, }, }, { name: "Service error", setupMock: func() { s.service.EXPECT(). ListContributors(gomock.Any()). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to list contributors"}, }, } for _, tc := range testCases { s.Run(tc.name, func() { tc.setupMock() req := httptest.NewRequest(http.MethodGet, "/api/v1/contributors", 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") } }) } } func (s *ContributorHandlerTestSuite) TestGetContributor() { testCases := []struct { name string id string setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", id: "1", setupMock: func() { s.service.EXPECT(). GetContributorByID(gomock.Any(), 1). Return(&ent.Contributor{ ID: 1, Name: "John Doe", Edges: ent.ContributorEdges{ SocialLinks: []*ent.ContributorSocialLink{ { Type: "github", Value: "https://github.com/johndoe", Edges: ent.ContributorSocialLinkEdges{}, }, }, }, CreatedAt: time.Time{}, UpdatedAt: time.Time{}, }, nil) }, expectedStatus: http.StatusOK, expectedBody: gin.H{ "id": 1, "name": "John Doe", "created_at": time.Time{}, "updated_at": time.Time{}, "edges": gin.H{ "social_links": []gin.H{ { "type": "github", "value": "https://github.com/johndoe", "edges": gin.H{}, }, }, }, }, }, { name: "Invalid ID", id: "invalid", setupMock: func() {}, expectedStatus: http.StatusBadRequest, expectedBody: gin.H{"error": "Invalid contributor ID"}, }, { name: "Service error", id: "1", setupMock: func() { s.service.EXPECT(). GetContributorByID(gomock.Any(), 1). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to get contributor"}, }, } for _, tc := range testCases { s.Run(tc.name, func() { tc.setupMock() req := httptest.NewRequest(http.MethodGet, "/api/v1/contributors/"+tc.id, 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") } }) } } func (s *ContributorHandlerTestSuite) TestCreateContributor() { testCases := []struct { name string body interface{} setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", body: CreateContributorRequest{ Name: "John Doe", }, setupMock: func() { name := "John Doe" s.service.EXPECT(). CreateContributor( gomock.Any(), name, nil, nil, ). Return(&ent.Contributor{ ID: 1, Name: name, CreatedAt: time.Time{}, UpdatedAt: time.Time{}, }, nil) }, expectedStatus: http.StatusCreated, expectedBody: gin.H{ "id": 1, "name": "John Doe", "created_at": time.Time{}, "updated_at": time.Time{}, "edges": gin.H{}, }, }, { name: "Invalid request body", body: map[string]interface{}{ "name": "", // Empty name is not allowed }, setupMock: func() {}, expectedStatus: http.StatusBadRequest, expectedBody: gin.H{"error": "Key: 'CreateContributorRequest.Name' Error:Field validation for 'Name' failed on the 'required' tag"}, }, { name: "Service error", body: CreateContributorRequest{ Name: "John Doe", }, setupMock: func() { name := "John Doe" s.service.EXPECT(). CreateContributor( gomock.Any(), name, nil, nil, ). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to create contributor"}, }, } 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/contributors", 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") } }) } } func (s *ContributorHandlerTestSuite) TestAddContributorSocialLink() { testCases := []struct { name string id string body interface{} setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", id: "1", body: func() AddContributorSocialLinkRequest { name := "johndoe" return AddContributorSocialLinkRequest{ Type: "github", Name: &name, Value: "https://github.com/johndoe", } }(), setupMock: func() { name := "johndoe" s.service.EXPECT(). AddContributorSocialLink( gomock.Any(), 1, "github", name, "https://github.com/johndoe", ). Return(&ent.ContributorSocialLink{ Type: "github", Name: name, Value: "https://github.com/johndoe", Edges: ent.ContributorSocialLinkEdges{}, }, nil) }, expectedStatus: http.StatusCreated, expectedBody: gin.H{ "type": "github", "name": "johndoe", "value": "https://github.com/johndoe", "edges": gin.H{}, }, }, { name: "Invalid contributor ID", id: "invalid", body: func() AddContributorSocialLinkRequest { name := "johndoe" return AddContributorSocialLinkRequest{ Type: "github", Name: &name, Value: "https://github.com/johndoe", } }(), setupMock: func() {}, expectedStatus: http.StatusBadRequest, expectedBody: gin.H{"error": "Invalid contributor ID"}, }, { name: "Invalid request body", id: "1", body: map[string]interface{}{ "type": "", // Empty type is not allowed }, setupMock: func() {}, expectedStatus: http.StatusBadRequest, expectedBody: gin.H{"error": "Key: 'AddContributorSocialLinkRequest.Type' Error:Field validation for 'Type' failed on the 'required' tag\nKey: 'AddContributorSocialLinkRequest.Value' Error:Field validation for 'Value' failed on the 'required' tag"}, }, { name: "Service error", id: "1", body: func() AddContributorSocialLinkRequest { name := "johndoe" return AddContributorSocialLinkRequest{ Type: "github", Name: &name, Value: "https://github.com/johndoe", } }(), setupMock: func() { name := "johndoe" s.service.EXPECT(). AddContributorSocialLink( gomock.Any(), 1, "github", name, "https://github.com/johndoe", ). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to add contributor social link"}, }, } 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/contributors/"+tc.id+"/social-links", 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") } }) } }