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 DailyHandlerTestSuite struct { suite.Suite ctrl *gomock.Controller service *mock.MockService handler *Handler router *gin.Engine } func (s *DailyHandlerTestSuite) 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 *DailyHandlerTestSuite) TearDownTest() { s.ctrl.Finish() } func TestDailyHandlerSuite(t *testing.T) { suite.Run(t, new(DailyHandlerTestSuite)) } func (s *DailyHandlerTestSuite) TestListDailies() { 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(). ListDailies(gomock.Any(), "en", nil, 10, 0). Return([]*ent.Daily{ { ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "en", Quote: "Test Quote 1", }, }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []*ent.Daily{ { ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "en", Quote: "Test Quote 1", }, }, }, }, }, }, { name: "Success with specific language", langCode: "zh", setupMock: func() { s.service.EXPECT(). ListDailies(gomock.Any(), "zh", nil, 10, 0). Return([]*ent.Daily{ { ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "zh", Quote: "测试语录1", }, }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []*ent.Daily{ { ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "zh", Quote: "测试语录1", }, }, }, }, }, }, { name: "Success with category filter", categoryID: "1", setupMock: func() { categoryID := 1 s.service.EXPECT(). ListDailies(gomock.Any(), "en", &categoryID, 10, 0). Return([]*ent.Daily{ { ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "en", Quote: "Test Quote 1", }, }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []*ent.Daily{ { ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "en", Quote: "Test Quote 1", }, }, }, }, }, }, { name: "Success with pagination", limit: "2", offset: "1", setupMock: func() { s.service.EXPECT(). ListDailies(gomock.Any(), "en", nil, 2, 1). Return([]*ent.Daily{ { ID: "daily2", ImageURL: "https://example.com/image2.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "en", Quote: "Test Quote 2", }, }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: []*ent.Daily{ { ID: "daily2", ImageURL: "https://example.com/image2.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "en", Quote: "Test Quote 2", }, }, }, }, }, }, { name: "Service Error", setupMock: func() { s.service.EXPECT(). ListDailies(gomock.Any(), "en", nil, 10, 0). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to list dailies"}, }, } for _, tc := range testCases { s.Run(tc.name, func() { tc.setupMock() url := "/api/v1/dailies" 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() 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 *DailyHandlerTestSuite) TestGetDaily() { testCases := []struct { name string id string setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", id: "daily1", setupMock: func() { s.service.EXPECT(). GetDailyByID(gomock.Any(), "daily1"). Return(&ent.Daily{ ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "en", Quote: "Test Quote 1", }, }, }, }, nil) }, expectedStatus: http.StatusOK, expectedBody: &ent.Daily{ ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{ { LanguageCode: "en", Quote: "Test Quote 1", }, }, }, }, }, { name: "Service error", id: "daily1", setupMock: func() { s.service.EXPECT(). GetDailyByID(gomock.Any(), "daily1"). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to get daily"}, }, } for _, tc := range testCases { s.Run(tc.name, func() { tc.setupMock() req := httptest.NewRequest(http.MethodGet, "/api/v1/dailies/"+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 *DailyHandlerTestSuite) TestCreateDaily() { testCases := []struct { name string body interface{} setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", body: CreateDailyRequest{ ID: "daily1", CategoryID: 1, ImageURL: "https://example.com/image1.jpg", }, setupMock: func() { s.service.EXPECT(). CreateDaily(gomock.Any(), "daily1", 1, "https://example.com/image1.jpg"). Return(&ent.Daily{ ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{}, }, }, nil) }, expectedStatus: http.StatusCreated, expectedBody: &ent.Daily{ ID: "daily1", ImageURL: "https://example.com/image1.jpg", Edges: ent.DailyEdges{ Category: &ent.Category{ID: 1}, Contents: []*ent.DailyContent{}, }, }, }, { name: "Invalid request body", body: map[string]interface{}{ "id": "daily1", // Missing required fields }, setupMock: func() {}, expectedStatus: http.StatusBadRequest, expectedBody: gin.H{"error": "Key: 'CreateDailyRequest.CategoryID' Error:Field validation for 'CategoryID' failed on the 'required' tag\nKey: 'CreateDailyRequest.ImageURL' Error:Field validation for 'ImageURL' failed on the 'required' tag"}, }, { name: "Service error", body: CreateDailyRequest{ ID: "daily1", CategoryID: 1, ImageURL: "https://example.com/image1.jpg", }, setupMock: func() { s.service.EXPECT(). CreateDaily(gomock.Any(), "daily1", 1, "https://example.com/image1.jpg"). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to create daily"}, }, } 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/dailies", 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 *DailyHandlerTestSuite) TestAddDailyContent() { testCases := []struct { name string dailyID string body interface{} setupMock func() expectedStatus int expectedBody interface{} }{ { name: "Success", dailyID: "daily1", body: AddDailyContentRequest{ LanguageCode: "en", Quote: "Test Quote 1", }, setupMock: func() { s.service.EXPECT(). AddDailyContent(gomock.Any(), "daily1", "en", "Test Quote 1"). Return(&ent.DailyContent{ LanguageCode: "en", Quote: "Test Quote 1", }, nil) }, expectedStatus: http.StatusCreated, expectedBody: &ent.DailyContent{ LanguageCode: "en", Quote: "Test Quote 1", }, }, { name: "Invalid request body", dailyID: "daily1", body: map[string]interface{}{ "language_code": "en", // Missing required fields }, setupMock: func() {}, expectedStatus: http.StatusBadRequest, expectedBody: gin.H{"error": "Key: 'AddDailyContentRequest.Quote' Error:Field validation for 'Quote' failed on the 'required' tag"}, }, { name: "Service error", dailyID: "daily1", body: AddDailyContentRequest{ LanguageCode: "en", Quote: "Test Quote 1", }, setupMock: func() { s.service.EXPECT(). AddDailyContent(gomock.Any(), "daily1", "en", "Test Quote 1"). Return(nil, errors.New("service error")) }, expectedStatus: http.StatusInternalServerError, expectedBody: gin.H{"error": "Failed to add daily 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/dailies/"+tc.dailyID+"/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") } }) } }