[feature] migrate to monorepo
This commit is contained in:
commit
05ddc1f783
267 changed files with 75165 additions and 0 deletions
119
backend/internal/handler/auth.go
Normal file
119
backend/internal/handler/auth.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=8"`
|
||||
Role string `json:"role" binding:"required,oneof=admin editor contributor"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func (h *Handler) Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.service.CreateUser(c.Request.Context(), req.Email, req.Password, req.Role)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create user")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user roles
|
||||
roles, err := h.service.GetUserRoles(c.Request.Context(), user.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get user roles")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user roles"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract role names for JWT
|
||||
roleNames := make([]string, len(roles))
|
||||
for i, r := range roles {
|
||||
roleNames[i] = r.Name
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": user.ID,
|
||||
"roles": roleNames,
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
})
|
||||
|
||||
tokenString, err := token.SignedString([]byte(h.cfg.JWT.Secret))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate token")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, AuthResponse{Token: tokenString})
|
||||
}
|
||||
|
||||
func (h *Handler) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.service.GetUserByEmail(c.Request.Context(), req.Email)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
if !h.service.ValidatePassword(c.Request.Context(), user, req.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user roles
|
||||
roles, err := h.service.GetUserRoles(c.Request.Context(), user.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get user roles")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user roles"})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract role names for JWT
|
||||
roleNames := make([]string, len(roles))
|
||||
for i, r := range roles {
|
||||
roleNames[i] = r.Name
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": user.ID,
|
||||
"roles": roleNames,
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
})
|
||||
|
||||
tokenString, err := token.SignedString([]byte(h.cfg.JWT.Secret))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to generate token")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, AuthResponse{Token: tokenString})
|
||||
}
|
276
backend/internal/handler/auth_handler_test.go
Normal file
276
backend/internal/handler/auth_handler_test.go
Normal file
|
@ -0,0 +1,276 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"tss-rocks-be/ent"
|
||||
"tss-rocks-be/internal/config"
|
||||
"tss-rocks-be/internal/service/mock"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
type AuthHandlerTestSuite struct {
|
||||
suite.Suite
|
||||
ctrl *gomock.Controller
|
||||
service *mock.MockService
|
||||
handler *Handler
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
func (s *AuthHandlerTestSuite) SetupTest() {
|
||||
s.ctrl = gomock.NewController(s.T())
|
||||
s.service = mock.NewMockService(s.ctrl)
|
||||
s.handler = NewHandler(&config.Config{
|
||||
JWT: config.JWTConfig{
|
||||
Secret: "test-secret",
|
||||
},
|
||||
}, s.service)
|
||||
s.router = gin.New()
|
||||
}
|
||||
|
||||
func (s *AuthHandlerTestSuite) TearDownTest() {
|
||||
s.ctrl.Finish()
|
||||
}
|
||||
|
||||
func TestAuthHandlerSuite(t *testing.T) {
|
||||
suite.Run(t, new(AuthHandlerTestSuite))
|
||||
}
|
||||
|
||||
func (s *AuthHandlerTestSuite) TestRegister() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
request RegisterRequest
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "成功注册",
|
||||
request: RegisterRequest{
|
||||
Email: "test@example.com",
|
||||
Password: "password123",
|
||||
Role: "contributor",
|
||||
},
|
||||
setupMock: func() {
|
||||
user := &ent.User{
|
||||
ID: 1,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
s.service.EXPECT().
|
||||
CreateUser(gomock.Any(), "test@example.com", "password123", "contributor").
|
||||
Return(user, nil)
|
||||
s.service.EXPECT().
|
||||
GetUserRoles(gomock.Any(), user.ID).
|
||||
Return([]*ent.Role{{ID: 1, Name: "contributor"}}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
name: "无效的邮箱格式",
|
||||
request: RegisterRequest{
|
||||
Email: "invalid-email",
|
||||
Password: "password123",
|
||||
Role: "contributor",
|
||||
},
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedError: "Key: 'RegisterRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag",
|
||||
},
|
||||
{
|
||||
name: "密码太短",
|
||||
request: RegisterRequest{
|
||||
Email: "test@example.com",
|
||||
Password: "short",
|
||||
Role: "contributor",
|
||||
},
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedError: "Key: 'RegisterRequest.Password' Error:Field validation for 'Password' failed on the 'min' tag",
|
||||
},
|
||||
{
|
||||
name: "无效的角色",
|
||||
request: RegisterRequest{
|
||||
Email: "test@example.com",
|
||||
Password: "password123",
|
||||
Role: "invalid-role",
|
||||
},
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedError: "Key: 'RegisterRequest.Role' Error:Field validation for 'Role' failed on the 'oneof' tag",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// 设置 mock
|
||||
tc.setupMock()
|
||||
|
||||
// 创建请求
|
||||
reqBody, _ := json.Marshal(tc.request)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/register", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// 执行请求
|
||||
s.handler.Register(c)
|
||||
|
||||
// 验证响应
|
||||
s.Equal(tc.expectedStatus, w.Code)
|
||||
if tc.expectedError != "" {
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.Contains(response["error"], tc.expectedError)
|
||||
} else {
|
||||
var response AuthResponse
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.NotEmpty(response.Token)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthHandlerTestSuite) TestLogin() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
request LoginRequest
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "成功登录",
|
||||
request: LoginRequest{
|
||||
Email: "test@example.com",
|
||||
Password: "password123",
|
||||
},
|
||||
setupMock: func() {
|
||||
user := &ent.User{
|
||||
ID: 1,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
s.service.EXPECT().
|
||||
GetUserByEmail(gomock.Any(), "test@example.com").
|
||||
Return(user, nil)
|
||||
s.service.EXPECT().
|
||||
ValidatePassword(gomock.Any(), user, "password123").
|
||||
Return(true)
|
||||
s.service.EXPECT().
|
||||
GetUserRoles(gomock.Any(), user.ID).
|
||||
Return([]*ent.Role{{ID: 1, Name: "contributor"}}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "无效的邮箱格式",
|
||||
request: LoginRequest{
|
||||
Email: "invalid-email",
|
||||
Password: "password123",
|
||||
},
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedError: "Key: 'LoginRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag",
|
||||
},
|
||||
{
|
||||
name: "用户不存在",
|
||||
request: LoginRequest{
|
||||
Email: "nonexistent@example.com",
|
||||
Password: "password123",
|
||||
},
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
GetUserByEmail(gomock.Any(), "nonexistent@example.com").
|
||||
Return(nil, errors.New("user not found"))
|
||||
},
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
expectedError: "Invalid credentials",
|
||||
},
|
||||
{
|
||||
name: "密码错误",
|
||||
request: LoginRequest{
|
||||
Email: "test@example.com",
|
||||
Password: "wrong-password",
|
||||
},
|
||||
setupMock: func() {
|
||||
user := &ent.User{
|
||||
ID: 1,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
s.service.EXPECT().
|
||||
GetUserByEmail(gomock.Any(), "test@example.com").
|
||||
Return(user, nil)
|
||||
s.service.EXPECT().
|
||||
ValidatePassword(gomock.Any(), user, "wrong-password").
|
||||
Return(false)
|
||||
},
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
expectedError: "Invalid credentials",
|
||||
},
|
||||
{
|
||||
name: "获取用户角色失败",
|
||||
request: LoginRequest{
|
||||
Email: "test@example.com",
|
||||
Password: "password123",
|
||||
},
|
||||
setupMock: func() {
|
||||
user := &ent.User{
|
||||
ID: 1,
|
||||
Email: "test@example.com",
|
||||
}
|
||||
s.service.EXPECT().
|
||||
GetUserByEmail(gomock.Any(), "test@example.com").
|
||||
Return(user, nil)
|
||||
s.service.EXPECT().
|
||||
ValidatePassword(gomock.Any(), user, "password123").
|
||||
Return(true)
|
||||
s.service.EXPECT().
|
||||
GetUserRoles(gomock.Any(), user.ID).
|
||||
Return(nil, errors.New("failed to get roles"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedError: "Failed to get user roles",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// 设置 mock
|
||||
tc.setupMock()
|
||||
|
||||
// 创建请求
|
||||
reqBody, _ := json.Marshal(tc.request)
|
||||
req, _ := http.NewRequest(http.MethodPost, "/login", bytes.NewBuffer(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// 执行请求
|
||||
s.handler.Login(c)
|
||||
|
||||
// 验证响应
|
||||
s.Equal(tc.expectedStatus, w.Code)
|
||||
if tc.expectedError != "" {
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.Contains(response["error"], tc.expectedError)
|
||||
} else {
|
||||
var response AuthResponse
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.NotEmpty(response.Token)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
468
backend/internal/handler/category_handler_test.go
Normal file
468
backend/internal/handler/category_handler_test.go
Normal file
|
@ -0,0 +1,468 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"tss-rocks-be/ent"
|
||||
"tss-rocks-be/ent/categorycontent"
|
||||
"tss-rocks-be/internal/config"
|
||||
"tss-rocks-be/internal/service/mock"
|
||||
"tss-rocks-be/internal/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/mock/gomock"
|
||||
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Custom assertion function for comparing categories
|
||||
func assertCategoryEqual(t assert.TestingT, expected, actual *ent.Category) bool {
|
||||
if expected == nil && actual == nil {
|
||||
return true
|
||||
}
|
||||
if expected == nil || actual == nil {
|
||||
return assert.Fail(t, "One category is nil while the other is not")
|
||||
}
|
||||
|
||||
// Compare only relevant fields, ignoring time fields
|
||||
return assert.Equal(t, expected.ID, actual.ID) &&
|
||||
assert.Equal(t, expected.Edges.Contents, actual.Edges.Contents)
|
||||
}
|
||||
|
||||
// Custom assertion function for comparing category slices
|
||||
func assertCategorySliceEqual(t assert.TestingT, expected, actual []*ent.Category) bool {
|
||||
if len(expected) != len(actual) {
|
||||
return assert.Fail(t, "Category slice lengths do not match")
|
||||
}
|
||||
|
||||
for i := range expected {
|
||||
if !assertCategoryEqual(t, expected[i], actual[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type CategoryHandlerTestSuite struct {
|
||||
suite.Suite
|
||||
ctrl *gomock.Controller
|
||||
service *mock.MockService
|
||||
handler *Handler
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
func (s *CategoryHandlerTestSuite) SetupTest() {
|
||||
s.ctrl = gomock.NewController(s.T())
|
||||
s.service = mock.NewMockService(s.ctrl)
|
||||
cfg := &config.Config{}
|
||||
s.handler = NewHandler(cfg, s.service)
|
||||
|
||||
// Setup Gin router
|
||||
gin.SetMode(gin.TestMode)
|
||||
s.router = gin.New()
|
||||
s.handler.RegisterRoutes(s.router)
|
||||
}
|
||||
|
||||
func (s *CategoryHandlerTestSuite) TearDownTest() {
|
||||
s.ctrl.Finish()
|
||||
}
|
||||
|
||||
func TestCategoryHandlerSuite(t *testing.T) {
|
||||
suite.Run(t, new(CategoryHandlerTestSuite))
|
||||
}
|
||||
|
||||
// Test cases for ListCategories
|
||||
func (s *CategoryHandlerTestSuite) TestListCategories() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
langCode string
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedBody interface{}
|
||||
}{
|
||||
{
|
||||
name: "Success with default language",
|
||||
langCode: "",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
ListCategories(gomock.Any(), gomock.Eq("en")).
|
||||
Return([]*ent.Category{
|
||||
{
|
||||
ID: 1,
|
||||
Edges: ent.CategoryEdges{
|
||||
Contents: []*ent.CategoryContent{
|
||||
{
|
||||
LanguageCode: categorycontent.LanguageCode("en"),
|
||||
Name: "Test Category",
|
||||
Description: "Test Description",
|
||||
Slug: "test-category",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []*ent.Category{
|
||||
{
|
||||
ID: 1,
|
||||
Edges: ent.CategoryEdges{
|
||||
Contents: []*ent.CategoryContent{
|
||||
{
|
||||
LanguageCode: categorycontent.LanguageCode("en"),
|
||||
Name: "Test Category",
|
||||
Description: "Test Description",
|
||||
Slug: "test-category",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Success with specific language",
|
||||
langCode: "zh",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
ListCategories(gomock.Any(), gomock.Eq("zh")).
|
||||
Return([]*ent.Category{
|
||||
{
|
||||
ID: 1,
|
||||
Edges: ent.CategoryEdges{
|
||||
Contents: []*ent.CategoryContent{
|
||||
{
|
||||
LanguageCode: categorycontent.LanguageCode("zh"),
|
||||
Name: "测试分类",
|
||||
Description: "测试描述",
|
||||
Slug: "test-category",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []*ent.Category{
|
||||
{
|
||||
ID: 1,
|
||||
Edges: ent.CategoryEdges{
|
||||
Contents: []*ent.CategoryContent{
|
||||
{
|
||||
LanguageCode: categorycontent.LanguageCode("zh"),
|
||||
Name: "测试分类",
|
||||
Description: "测试描述",
|
||||
Slug: "test-category",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// Setup mock
|
||||
tc.setupMock()
|
||||
|
||||
// Create request
|
||||
url := "/api/v1/categories"
|
||||
if tc.langCode != "" {
|
||||
url += "?lang=" + tc.langCode
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Perform request
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Assert response
|
||||
assert.Equal(s.T(), tc.expectedStatus, w.Code)
|
||||
if tc.expectedBody != nil {
|
||||
var response []*ent.Category
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(s.T(), err)
|
||||
assertCategorySliceEqual(s.T(), tc.expectedBody.([]*ent.Category), response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test cases for GetCategory
|
||||
func (s *CategoryHandlerTestSuite) TestGetCategory() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
langCode string
|
||||
slug string
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedBody interface{}
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
langCode: "en",
|
||||
slug: "test-category",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
GetCategoryBySlug(gomock.Any(), gomock.Eq("en"), gomock.Eq("test-category")).
|
||||
Return(&ent.Category{
|
||||
ID: 1,
|
||||
Edges: ent.CategoryEdges{
|
||||
Contents: []*ent.CategoryContent{
|
||||
{
|
||||
LanguageCode: categorycontent.LanguageCode("en"),
|
||||
Name: "Test Category",
|
||||
Description: "Test Description",
|
||||
Slug: "test-category",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: &ent.Category{
|
||||
ID: 1,
|
||||
Edges: ent.CategoryEdges{
|
||||
Contents: []*ent.CategoryContent{
|
||||
{
|
||||
LanguageCode: categorycontent.LanguageCode("en"),
|
||||
Name: "Test Category",
|
||||
Description: "Test Description",
|
||||
Slug: "test-category",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Not Found",
|
||||
langCode: "en",
|
||||
slug: "non-existent",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
GetCategoryBySlug(gomock.Any(), gomock.Eq("en"), gomock.Eq("non-existent")).
|
||||
Return(nil, types.ErrNotFound)
|
||||
},
|
||||
expectedStatus: http.StatusNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// Setup mock
|
||||
tc.setupMock()
|
||||
|
||||
// Create request
|
||||
url := "/api/v1/categories/" + tc.slug
|
||||
if tc.langCode != "" {
|
||||
url += "?lang=" + tc.langCode
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Perform request
|
||||
s.router.ServeHTTP(w, req)
|
||||
|
||||
// Assert response
|
||||
assert.Equal(s.T(), tc.expectedStatus, w.Code)
|
||||
if tc.expectedBody != nil {
|
||||
var response ent.Category
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(s.T(), err)
|
||||
assertCategoryEqual(s.T(), tc.expectedBody.(*ent.Category), &response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test cases for AddCategoryContent
|
||||
func (s *CategoryHandlerTestSuite) TestAddCategoryContent() {
|
||||
var description = "Test Description"
|
||||
testCases := []struct {
|
||||
name string
|
||||
categoryID string
|
||||
requestBody interface{}
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedBody interface{}
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
categoryID: "1",
|
||||
requestBody: AddCategoryContentRequest{
|
||||
LanguageCode: "en",
|
||||
Name: "Test Category",
|
||||
Description: &description,
|
||||
Slug: "test-category",
|
||||
},
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
AddCategoryContent(
|
||||
gomock.Any(),
|
||||
1,
|
||||
"en",
|
||||
"Test Category",
|
||||
description,
|
||||
"test-category",
|
||||
).
|
||||
Return(&ent.CategoryContent{
|
||||
LanguageCode: categorycontent.LanguageCode("en"),
|
||||
Name: "Test Category",
|
||||
Description: description,
|
||||
Slug: "test-category",
|
||||
}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusCreated,
|
||||
expectedBody: &ent.CategoryContent{
|
||||
LanguageCode: categorycontent.LanguageCode("en"),
|
||||
Name: "Test Category",
|
||||
Description: description,
|
||||
Slug: "test-category",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid JSON",
|
||||
categoryID: "1",
|
||||
requestBody: "invalid json",
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Invalid Category ID",
|
||||
categoryID: "invalid",
|
||||
requestBody: AddCategoryContentRequest{
|
||||
LanguageCode: "en",
|
||||
Name: "Test Category",
|
||||
Description: &description,
|
||||
Slug: "test-category",
|
||||
},
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Service Error",
|
||||
categoryID: "1",
|
||||
requestBody: AddCategoryContentRequest{
|
||||
LanguageCode: "en",
|
||||
Name: "Test Category",
|
||||
Description: &description,
|
||||
Slug: "test-category",
|
||||
},
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
AddCategoryContent(
|
||||
gomock.Any(),
|
||||
1,
|
||||
"en",
|
||||
"Test Category",
|
||||
description,
|
||||
"test-category",
|
||||
).
|
||||
Return(nil, errors.New("service error"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// Setup mock
|
||||
tc.setupMock()
|
||||
|
||||
// Create request
|
||||
var body []byte
|
||||
var err error
|
||||
if str, ok := tc.requestBody.(string); ok {
|
||||
body = []byte(str)
|
||||
} else {
|
||||
body, err = json.Marshal(tc.requestBody)
|
||||
s.NoError(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/categories/"+tc.categoryID+"/contents", bytes.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
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.CategoryContent
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.Equal(tc.expectedBody, &response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test cases for CreateCategory
|
||||
func (s *CategoryHandlerTestSuite) TestCreateCategory() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "成功创建分类",
|
||||
setupMock: func() {
|
||||
category := &ent.Category{
|
||||
ID: 1,
|
||||
}
|
||||
s.service.EXPECT().
|
||||
CreateCategory(gomock.Any()).
|
||||
Return(category, nil)
|
||||
},
|
||||
expectedStatus: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
name: "创建分类失败",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
CreateCategory(gomock.Any()).
|
||||
Return(nil, errors.New("failed to create category"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedError: "Failed to create category",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// 设置 mock
|
||||
tc.setupMock()
|
||||
|
||||
// 创建请求
|
||||
req, _ := http.NewRequest(http.MethodPost, "/categories", nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// 执行请求
|
||||
s.handler.CreateCategory(c)
|
||||
|
||||
// 验证响应
|
||||
s.Equal(tc.expectedStatus, w.Code)
|
||||
if tc.expectedError != "" {
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.Equal(tc.expectedError, response["error"])
|
||||
} else {
|
||||
var response *ent.Category
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.NotNil(response)
|
||||
s.Equal(1, response.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
443
backend/internal/handler/contributor_handler_test.go
Normal file
443
backend/internal/handler/contributor_handler_test.go
Normal file
|
@ -0,0 +1,443 @@
|
|||
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/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{}
|
||||
s.handler = NewHandler(cfg, s.service)
|
||||
|
||||
// Setup Gin router
|
||||
gin.SetMode(gin.TestMode)
|
||||
s.router = gin.New()
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
519
backend/internal/handler/daily_handler_test.go
Normal file
519
backend/internal/handler/daily_handler_test.go
Normal file
|
@ -0,0 +1,519 @@
|
|||
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/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{}
|
||||
s.handler = NewHandler(cfg, s.service)
|
||||
|
||||
// Setup Gin router
|
||||
gin.SetMode(gin.TestMode)
|
||||
s.router = gin.New()
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
513
backend/internal/handler/handler.go
Normal file
513
backend/internal/handler/handler.go
Normal file
|
@ -0,0 +1,513 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"tss-rocks-be/internal/config"
|
||||
"tss-rocks-be/internal/service"
|
||||
"tss-rocks-be/internal/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
cfg *config.Config
|
||||
service service.Service
|
||||
}
|
||||
|
||||
func NewHandler(cfg *config.Config, service service.Service) *Handler {
|
||||
return &Handler{
|
||||
cfg: cfg,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all the routes
|
||||
func (h *Handler) RegisterRoutes(r *gin.Engine) {
|
||||
api := r.Group("/api/v1")
|
||||
{
|
||||
// Auth routes
|
||||
auth := api.Group("/auth")
|
||||
{
|
||||
auth.POST("/register", h.Register)
|
||||
auth.POST("/login", h.Login)
|
||||
}
|
||||
|
||||
// Category routes
|
||||
categories := api.Group("/categories")
|
||||
{
|
||||
categories.GET("", h.ListCategories)
|
||||
categories.GET("/:slug", h.GetCategory)
|
||||
categories.POST("", h.CreateCategory)
|
||||
categories.POST("/:id/contents", h.AddCategoryContent)
|
||||
}
|
||||
|
||||
// Post routes
|
||||
posts := api.Group("/posts")
|
||||
{
|
||||
posts.GET("", h.ListPosts)
|
||||
posts.GET("/:slug", h.GetPost)
|
||||
posts.POST("", h.CreatePost)
|
||||
posts.POST("/:id/contents", h.AddPostContent)
|
||||
}
|
||||
|
||||
// Contributor routes
|
||||
contributors := api.Group("/contributors")
|
||||
{
|
||||
contributors.GET("", h.ListContributors)
|
||||
contributors.GET("/:id", h.GetContributor)
|
||||
contributors.POST("", h.CreateContributor)
|
||||
contributors.POST("/:id/social-links", h.AddContributorSocialLink)
|
||||
}
|
||||
|
||||
// Daily routes
|
||||
dailies := api.Group("/dailies")
|
||||
{
|
||||
dailies.GET("", h.ListDailies)
|
||||
dailies.GET("/:id", h.GetDaily)
|
||||
dailies.POST("", h.CreateDaily)
|
||||
dailies.POST("/:id/contents", h.AddDailyContent)
|
||||
}
|
||||
|
||||
// Media routes
|
||||
media := api.Group("/media")
|
||||
{
|
||||
media.GET("", h.ListMedia)
|
||||
media.POST("", h.UploadMedia)
|
||||
media.GET("/:id", h.GetMedia)
|
||||
media.DELETE("/:id", h.DeleteMedia)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Category handlers
|
||||
func (h *Handler) ListCategories(c *gin.Context) {
|
||||
langCode := c.Query("lang")
|
||||
if langCode == "" {
|
||||
langCode = "en" // Default to English
|
||||
}
|
||||
|
||||
categories, err := h.service.ListCategories(c.Request.Context(), langCode)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list categories")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list categories"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, categories)
|
||||
}
|
||||
|
||||
func (h *Handler) GetCategory(c *gin.Context) {
|
||||
langCode := c.Query("lang")
|
||||
if langCode == "" {
|
||||
langCode = "en" // Default to English
|
||||
}
|
||||
|
||||
slug := c.Param("slug")
|
||||
category, err := h.service.GetCategoryBySlug(c.Request.Context(), langCode, slug)
|
||||
if err != nil {
|
||||
if err == types.ErrNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
|
||||
return
|
||||
}
|
||||
log.Error().Err(err).Msg("Failed to get category")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get category"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, category)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateCategory(c *gin.Context) {
|
||||
category, err := h.service.CreateCategory(c.Request.Context())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create category")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create category"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, category)
|
||||
}
|
||||
|
||||
type AddCategoryContentRequest struct {
|
||||
LanguageCode string `json:"language_code" binding:"required"`
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description *string `json:"description"`
|
||||
Slug string `json:"slug" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *Handler) AddCategoryContent(c *gin.Context) {
|
||||
var req AddCategoryContentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
categoryID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var description string
|
||||
if req.Description != nil {
|
||||
description = *req.Description
|
||||
}
|
||||
|
||||
content, err := h.service.AddCategoryContent(c.Request.Context(), categoryID, req.LanguageCode, req.Name, description, req.Slug)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to add category content")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add category content"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, content)
|
||||
}
|
||||
|
||||
// Post handlers
|
||||
func (h *Handler) ListPosts(c *gin.Context) {
|
||||
langCode := c.Query("lang")
|
||||
if langCode == "" {
|
||||
langCode = "en" // Default to English
|
||||
}
|
||||
|
||||
var categoryID *int
|
||||
if catIDStr := c.Query("category_id"); catIDStr != "" {
|
||||
if id, err := strconv.Atoi(catIDStr); err == nil {
|
||||
categoryID = &id
|
||||
}
|
||||
}
|
||||
|
||||
limit := 10 // Default limit
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0 // Default offset
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
||||
offset = o
|
||||
}
|
||||
}
|
||||
|
||||
posts, err := h.service.ListPosts(c.Request.Context(), langCode, categoryID, limit, offset)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list posts")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list posts"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, posts)
|
||||
}
|
||||
|
||||
func (h *Handler) GetPost(c *gin.Context) {
|
||||
langCode := c.Query("lang")
|
||||
if langCode == "" {
|
||||
langCode = "en" // Default to English
|
||||
}
|
||||
|
||||
slug := c.Param("slug")
|
||||
post, err := h.service.GetPostBySlug(c.Request.Context(), langCode, slug)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get post")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get post"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to a map to control the fields
|
||||
response := gin.H{
|
||||
"id": post.ID,
|
||||
"status": post.Status,
|
||||
"slug": post.Slug,
|
||||
"edges": gin.H{
|
||||
"contents": []gin.H{},
|
||||
},
|
||||
}
|
||||
|
||||
contents := make([]gin.H, 0, len(post.Edges.Contents))
|
||||
for _, content := range post.Edges.Contents {
|
||||
contents = append(contents, gin.H{
|
||||
"language_code": content.LanguageCode,
|
||||
"title": content.Title,
|
||||
"content_markdown": content.ContentMarkdown,
|
||||
"summary": content.Summary,
|
||||
})
|
||||
}
|
||||
response["edges"].(gin.H)["contents"] = contents
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *Handler) CreatePost(c *gin.Context) {
|
||||
post, err := h.service.CreatePost(c.Request.Context(), "draft") // Default to draft status
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create post")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create post"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to a map to control the fields
|
||||
response := gin.H{
|
||||
"id": post.ID,
|
||||
"status": post.Status,
|
||||
"edges": gin.H{
|
||||
"contents": []interface{}{},
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
type AddPostContentRequest struct {
|
||||
LanguageCode string `json:"language_code" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
ContentMarkdown string `json:"content_markdown" binding:"required"`
|
||||
Summary string `json:"summary" binding:"required"`
|
||||
MetaKeywords string `json:"meta_keywords"`
|
||||
MetaDescription string `json:"meta_description"`
|
||||
}
|
||||
|
||||
func (h *Handler) AddPostContent(c *gin.Context) {
|
||||
var req AddPostContentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
postID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid post ID"})
|
||||
return
|
||||
}
|
||||
|
||||
content, err := h.service.AddPostContent(c.Request.Context(), postID, req.LanguageCode, req.Title, req.ContentMarkdown, req.Summary, req.MetaKeywords, req.MetaDescription)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to add post content")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add post content"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"title": content.Title,
|
||||
"content_markdown": content.ContentMarkdown,
|
||||
"language_code": content.LanguageCode,
|
||||
"summary": content.Summary,
|
||||
"meta_keywords": content.MetaKeywords,
|
||||
"meta_description": content.MetaDescription,
|
||||
"edges": gin.H{},
|
||||
})
|
||||
}
|
||||
|
||||
// Contributor handlers
|
||||
func (h *Handler) ListContributors(c *gin.Context) {
|
||||
contributors, err := h.service.ListContributors(c.Request.Context())
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list contributors")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list contributors"})
|
||||
return
|
||||
}
|
||||
|
||||
response := make([]gin.H, len(contributors))
|
||||
for i, contributor := range contributors {
|
||||
socialLinks := make([]gin.H, len(contributor.Edges.SocialLinks))
|
||||
for j, link := range contributor.Edges.SocialLinks {
|
||||
socialLinks[j] = gin.H{
|
||||
"type": link.Type,
|
||||
"value": link.Value,
|
||||
"edges": gin.H{},
|
||||
}
|
||||
}
|
||||
|
||||
response[i] = gin.H{
|
||||
"id": contributor.ID,
|
||||
"name": contributor.Name,
|
||||
"created_at": contributor.CreatedAt,
|
||||
"updated_at": contributor.UpdatedAt,
|
||||
"edges": gin.H{
|
||||
"social_links": socialLinks,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *Handler) GetContributor(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contributor ID"})
|
||||
return
|
||||
}
|
||||
|
||||
contributor, err := h.service.GetContributorByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get contributor")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get contributor"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, contributor)
|
||||
}
|
||||
|
||||
type CreateContributorRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
Bio *string `json:"bio"`
|
||||
}
|
||||
|
||||
func (h *Handler) CreateContributor(c *gin.Context) {
|
||||
var req CreateContributorRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contributor, err := h.service.CreateContributor(c.Request.Context(), req.Name, req.AvatarURL, req.Bio)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create contributor")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create contributor"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, contributor)
|
||||
}
|
||||
|
||||
type AddContributorSocialLinkRequest struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Name *string `json:"name"`
|
||||
Value string `json:"value" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *Handler) AddContributorSocialLink(c *gin.Context) {
|
||||
var req AddContributorSocialLinkRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
contributorID, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid contributor ID"})
|
||||
return
|
||||
}
|
||||
|
||||
name := ""
|
||||
if req.Name != nil {
|
||||
name = *req.Name
|
||||
}
|
||||
link, err := h.service.AddContributorSocialLink(c.Request.Context(), contributorID, req.Type, name, req.Value)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to add contributor social link")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add contributor social link"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, link)
|
||||
}
|
||||
|
||||
// Daily handlers
|
||||
func (h *Handler) ListDailies(c *gin.Context) {
|
||||
langCode := c.Query("lang")
|
||||
if langCode == "" {
|
||||
langCode = "en" // Default to English
|
||||
}
|
||||
|
||||
var categoryID *int
|
||||
if catIDStr := c.Query("category_id"); catIDStr != "" {
|
||||
if id, err := strconv.Atoi(catIDStr); err == nil {
|
||||
categoryID = &id
|
||||
}
|
||||
}
|
||||
|
||||
limit := 10 // Default limit
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0 // Default offset
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
||||
offset = o
|
||||
}
|
||||
}
|
||||
|
||||
dailies, err := h.service.ListDailies(c.Request.Context(), langCode, categoryID, limit, offset)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list dailies")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list dailies"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, dailies)
|
||||
}
|
||||
|
||||
func (h *Handler) GetDaily(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
daily, err := h.service.GetDailyByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get daily")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get daily"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, daily)
|
||||
}
|
||||
|
||||
type CreateDailyRequest struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
CategoryID int `json:"category_id" binding:"required"`
|
||||
ImageURL string `json:"image_url" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *Handler) CreateDaily(c *gin.Context) {
|
||||
var req CreateDailyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
daily, err := h.service.CreateDaily(c.Request.Context(), req.ID, req.CategoryID, req.ImageURL)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create daily")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create daily"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, daily)
|
||||
}
|
||||
|
||||
type AddDailyContentRequest struct {
|
||||
LanguageCode string `json:"language_code" binding:"required"`
|
||||
Quote string `json:"quote" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *Handler) AddDailyContent(c *gin.Context) {
|
||||
var req AddDailyContentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
dailyID := c.Param("id")
|
||||
content, err := h.service.AddDailyContent(c.Request.Context(), dailyID, req.LanguageCode, req.Quote)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to add daily content")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to add daily content"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, content)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func stringPtr(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
43
backend/internal/handler/handler_test.go
Normal file
43
backend/internal/handler/handler_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStringPtr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input *string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "nil pointer",
|
||||
input: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: strPtr(""),
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "non-empty string",
|
||||
input: strPtr("test"),
|
||||
expected: "test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := stringPtr(tc.input)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create string pointer
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
173
backend/internal/handler/media.go
Normal file
173
backend/internal/handler/media.go
Normal file
|
@ -0,0 +1,173 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Media handlers
|
||||
func (h *Handler) ListMedia(c *gin.Context) {
|
||||
limit := 10 // Default limit
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
offset := 0 // Default offset
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
if o, err := strconv.Atoi(offsetStr); err == nil && o >= 0 {
|
||||
offset = o
|
||||
}
|
||||
}
|
||||
|
||||
media, err := h.service.ListMedia(c.Request.Context(), limit, offset)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list media")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list media"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, media)
|
||||
}
|
||||
|
||||
func (h *Handler) UploadMedia(c *gin.Context) {
|
||||
// Get user ID from context (set by auth middleware)
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get file from form
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "No file uploaded"})
|
||||
return
|
||||
}
|
||||
|
||||
// 文件大小限制
|
||||
if file.Size > 10*1024*1024 { // 10MB
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "File size exceeds the limit (10MB)"})
|
||||
return
|
||||
}
|
||||
|
||||
// 文件类型限制
|
||||
allowedTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"video/mp4": true,
|
||||
"video/webm": true,
|
||||
"audio/mpeg": true,
|
||||
"audio/ogg": true,
|
||||
"application/pdf": true,
|
||||
}
|
||||
contentType := file.Header.Get("Content-Type")
|
||||
if _, ok := allowedTypes[contentType]; !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid file type"})
|
||||
return
|
||||
}
|
||||
|
||||
// Upload file
|
||||
media, err := h.service.Upload(c.Request.Context(), file, userID.(int))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to upload media")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload media"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, media)
|
||||
}
|
||||
|
||||
func (h *Handler) GetMedia(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid media ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get media metadata
|
||||
media, err := h.service.GetMedia(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get media")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get media"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get file content
|
||||
reader, info, err := h.service.GetFile(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get media file")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get media file"})
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Set response headers
|
||||
c.Header("Content-Type", media.MimeType)
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", info.Size))
|
||||
c.Header("Content-Disposition", fmt.Sprintf("inline; filename=%s", media.OriginalName))
|
||||
|
||||
// Stream the file
|
||||
if _, err := io.Copy(c.Writer, reader); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to stream media file")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) GetMediaFile(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid media ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get file content
|
||||
reader, info, err := h.service.GetFile(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get media file")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get media file"})
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Set response headers
|
||||
c.Header("Content-Type", info.ContentType)
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", info.Size))
|
||||
c.Header("Content-Disposition", fmt.Sprintf("inline; filename=%s", info.Name))
|
||||
|
||||
// Stream the file
|
||||
if _, err := io.Copy(c.Writer, reader); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to stream media file")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) DeleteMedia(c *gin.Context) {
|
||||
// Get user ID from context (set by auth middleware)
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid media ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.service.DeleteMedia(c.Request.Context(), id, userID.(int)); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete media")
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete media"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusNoContent, nil)
|
||||
}
|
524
backend/internal/handler/media_handler_test.go
Normal file
524
backend/internal/handler/media_handler_test.go
Normal file
|
@ -0,0 +1,524 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"tss-rocks-be/ent"
|
||||
"tss-rocks-be/internal/config"
|
||||
"tss-rocks-be/internal/service/mock"
|
||||
"tss-rocks-be/internal/storage"
|
||||
|
||||
"net/textproto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
type MediaHandlerTestSuite struct {
|
||||
suite.Suite
|
||||
ctrl *gomock.Controller
|
||||
service *mock.MockService
|
||||
handler *Handler
|
||||
router *gin.Engine
|
||||
}
|
||||
|
||||
func (s *MediaHandlerTestSuite) SetupTest() {
|
||||
s.ctrl = gomock.NewController(s.T())
|
||||
s.service = mock.NewMockService(s.ctrl)
|
||||
s.handler = NewHandler(&config.Config{}, s.service)
|
||||
s.router = gin.New()
|
||||
}
|
||||
|
||||
func (s *MediaHandlerTestSuite) TearDownTest() {
|
||||
s.ctrl.Finish()
|
||||
}
|
||||
|
||||
func TestMediaHandlerSuite(t *testing.T) {
|
||||
suite.Run(t, new(MediaHandlerTestSuite))
|
||||
}
|
||||
|
||||
func (s *MediaHandlerTestSuite) TestListMedia() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
query string
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "成功列出媒体",
|
||||
query: "?limit=10&offset=0",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
ListMedia(gomock.Any(), 10, 0).
|
||||
Return([]*ent.Media{{ID: 1}}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "使用默认限制和偏移",
|
||||
query: "",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
ListMedia(gomock.Any(), 10, 0).
|
||||
Return([]*ent.Media{{ID: 1}}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "列出媒体失败",
|
||||
query: "",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
ListMedia(gomock.Any(), 10, 0).
|
||||
Return(nil, errors.New("failed to list media"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedError: "Failed to list media",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// 设置 mock
|
||||
tc.setupMock()
|
||||
|
||||
// 创建请求
|
||||
req, _ := http.NewRequest(http.MethodGet, "/media"+tc.query, nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// 执行请求
|
||||
s.handler.ListMedia(c)
|
||||
|
||||
// 验证响应
|
||||
s.Equal(tc.expectedStatus, w.Code)
|
||||
if tc.expectedError != "" {
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.Equal(tc.expectedError, response["error"])
|
||||
} else {
|
||||
var response []*ent.Media
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.NotEmpty(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MediaHandlerTestSuite) TestUploadMedia() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
setupRequest func() (*http.Request, error)
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "成功上传媒体",
|
||||
setupRequest: func() (*http.Request, error) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// 创建文件部分
|
||||
fileHeader := make(textproto.MIMEHeader)
|
||||
fileHeader.Set("Content-Type", "image/jpeg")
|
||||
fileHeader.Set("Content-Disposition", `form-data; name="file"; filename="test.jpg"`)
|
||||
part, err := writer.CreatePart(fileHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
testContent := "test content"
|
||||
_, err = io.Copy(part, strings.NewReader(testContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/media", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
return req, nil
|
||||
},
|
||||
setupMock: func() {
|
||||
expectedFile := &multipart.FileHeader{
|
||||
Filename: "test.jpg",
|
||||
Size: int64(len("test content")),
|
||||
Header: textproto.MIMEHeader{
|
||||
"Content-Type": []string{"image/jpeg"},
|
||||
},
|
||||
}
|
||||
s.service.EXPECT().
|
||||
Upload(gomock.Any(), gomock.Any(), 1).
|
||||
DoAndReturn(func(_ context.Context, f *multipart.FileHeader, uid int) (*ent.Media, error) {
|
||||
s.Equal(expectedFile.Filename, f.Filename)
|
||||
s.Equal(expectedFile.Size, f.Size)
|
||||
s.Equal(expectedFile.Header.Get("Content-Type"), f.Header.Get("Content-Type"))
|
||||
return &ent.Media{ID: 1}, nil
|
||||
})
|
||||
},
|
||||
expectedStatus: http.StatusCreated,
|
||||
},
|
||||
{
|
||||
name: "未授权",
|
||||
setupRequest: func() (*http.Request, error) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/media", nil)
|
||||
return req, nil
|
||||
},
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
expectedError: "Unauthorized",
|
||||
},
|
||||
{
|
||||
name: "上传失败",
|
||||
setupRequest: func() (*http.Request, error) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// 创建文件部分
|
||||
fileHeader := make(textproto.MIMEHeader)
|
||||
fileHeader.Set("Content-Type", "image/jpeg")
|
||||
fileHeader.Set("Content-Disposition", `form-data; name="file"; filename="test.jpg"`)
|
||||
part, err := writer.CreatePart(fileHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
testContent := "test content"
|
||||
_, err = io.Copy(part, strings.NewReader(testContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
writer.Close()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/media", body)
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
return req, nil
|
||||
},
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
Upload(gomock.Any(), gomock.Any(), 1).
|
||||
Return(nil, errors.New("failed to upload"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedError: "Failed to upload media",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// 设置 mock
|
||||
tc.setupMock()
|
||||
|
||||
// 创建请求
|
||||
req, err := tc.setupRequest()
|
||||
s.NoError(err)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// 设置用户ID(除了未授权的测试用例)
|
||||
if tc.expectedError != "Unauthorized" {
|
||||
c.Set("user_id", 1)
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
s.handler.UploadMedia(c)
|
||||
|
||||
// 验证响应
|
||||
s.Equal(tc.expectedStatus, w.Code)
|
||||
if tc.expectedError != "" {
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.Equal(tc.expectedError, response["error"])
|
||||
} else {
|
||||
var response *ent.Media
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.NotNil(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MediaHandlerTestSuite) TestGetMedia() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
mediaID string
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "成功获取媒体",
|
||||
mediaID: "1",
|
||||
setupMock: func() {
|
||||
media := &ent.Media{
|
||||
ID: 1,
|
||||
MimeType: "image/jpeg",
|
||||
OriginalName: "test.jpg",
|
||||
}
|
||||
s.service.EXPECT().
|
||||
GetMedia(gomock.Any(), 1).
|
||||
Return(media, nil)
|
||||
s.service.EXPECT().
|
||||
GetFile(gomock.Any(), 1).
|
||||
Return(io.NopCloser(strings.NewReader("test content")), &storage.FileInfo{
|
||||
Size: 11,
|
||||
Name: "test.jpg",
|
||||
ContentType: "image/jpeg",
|
||||
}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "无效的媒体ID",
|
||||
mediaID: "invalid",
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedError: "Invalid media ID",
|
||||
},
|
||||
{
|
||||
name: "获取媒体元数据失败",
|
||||
mediaID: "1",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
GetMedia(gomock.Any(), 1).
|
||||
Return(nil, errors.New("failed to get media"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedError: "Failed to get media",
|
||||
},
|
||||
{
|
||||
name: "获取媒体文件失败",
|
||||
mediaID: "1",
|
||||
setupMock: func() {
|
||||
media := &ent.Media{
|
||||
ID: 1,
|
||||
MimeType: "image/jpeg",
|
||||
OriginalName: "test.jpg",
|
||||
}
|
||||
s.service.EXPECT().
|
||||
GetMedia(gomock.Any(), 1).
|
||||
Return(media, nil)
|
||||
s.service.EXPECT().
|
||||
GetFile(gomock.Any(), 1).
|
||||
Return(nil, nil, errors.New("failed to get file"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedError: "Failed to get media file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// 设置 mock
|
||||
tc.setupMock()
|
||||
|
||||
// 创建请求
|
||||
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/media/%s", tc.mediaID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Extract ID from URL path
|
||||
parts := strings.Split(strings.Trim(req.URL.Path, "/"), "/")
|
||||
if len(parts) >= 2 {
|
||||
c.Params = []gin.Param{{Key: "id", Value: parts[1]}}
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
s.handler.GetMedia(c)
|
||||
|
||||
// 验证响应
|
||||
s.Equal(tc.expectedStatus, w.Code)
|
||||
if tc.expectedError != "" {
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.Equal(tc.expectedError, response["error"])
|
||||
} else {
|
||||
s.Equal("image/jpeg", w.Header().Get("Content-Type"))
|
||||
s.Equal("11", w.Header().Get("Content-Length"))
|
||||
s.Equal("inline; filename=test.jpg", w.Header().Get("Content-Disposition"))
|
||||
s.Equal("test content", w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MediaHandlerTestSuite) TestGetMediaFile() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
setupRequest func() (*http.Request, error)
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedBody []byte
|
||||
}{
|
||||
{
|
||||
name: "成功获取媒体文件",
|
||||
setupRequest: func() (*http.Request, error) {
|
||||
return httptest.NewRequest(http.MethodGet, "/media/1/file", nil), nil
|
||||
},
|
||||
setupMock: func() {
|
||||
fileContent := "test file content"
|
||||
s.service.EXPECT().
|
||||
GetFile(gomock.Any(), 1).
|
||||
Return(io.NopCloser(strings.NewReader(fileContent)), &storage.FileInfo{
|
||||
Name: "test.jpg",
|
||||
Size: int64(len(fileContent)),
|
||||
ContentType: "image/jpeg",
|
||||
}, nil)
|
||||
},
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: []byte("test file content"),
|
||||
},
|
||||
{
|
||||
name: "无效的媒体ID",
|
||||
setupRequest: func() (*http.Request, error) {
|
||||
return httptest.NewRequest(http.MethodGet, "/media/invalid/file", nil), nil
|
||||
},
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "获取媒体文件失败",
|
||||
setupRequest: func() (*http.Request, error) {
|
||||
return httptest.NewRequest(http.MethodGet, "/media/1/file", nil), nil
|
||||
},
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
GetFile(gomock.Any(), 1).
|
||||
Return(nil, nil, errors.New("failed to get file"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// Setup
|
||||
req, err := tc.setupRequest()
|
||||
s.Require().NoError(err)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Extract ID from URL path
|
||||
parts := strings.Split(strings.Trim(req.URL.Path, "/"), "/")
|
||||
if len(parts) >= 2 {
|
||||
c.Params = []gin.Param{{Key: "id", Value: parts[1]}}
|
||||
}
|
||||
|
||||
// Setup mock
|
||||
tc.setupMock()
|
||||
|
||||
// Test
|
||||
s.handler.GetMediaFile(c)
|
||||
|
||||
// Verify
|
||||
s.Equal(tc.expectedStatus, w.Code)
|
||||
if tc.expectedBody != nil {
|
||||
s.Equal(tc.expectedBody, w.Body.Bytes())
|
||||
s.Equal("image/jpeg", w.Header().Get("Content-Type"))
|
||||
s.Equal(fmt.Sprintf("%d", len(tc.expectedBody)), w.Header().Get("Content-Length"))
|
||||
s.Equal("inline; filename=test.jpg", w.Header().Get("Content-Disposition"))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MediaHandlerTestSuite) TestDeleteMedia() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
mediaID string
|
||||
setupMock func()
|
||||
expectedStatus int
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "成功删除媒体",
|
||||
mediaID: "1",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
DeleteMedia(gomock.Any(), 1, 1).
|
||||
Return(nil)
|
||||
},
|
||||
expectedStatus: http.StatusNoContent,
|
||||
},
|
||||
{
|
||||
name: "未授权",
|
||||
mediaID: "1",
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
expectedError: "Unauthorized",
|
||||
},
|
||||
{
|
||||
name: "无效的媒体ID",
|
||||
mediaID: "invalid",
|
||||
setupMock: func() {},
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectedError: "Invalid media ID",
|
||||
},
|
||||
{
|
||||
name: "删除媒体失败",
|
||||
mediaID: "1",
|
||||
setupMock: func() {
|
||||
s.service.EXPECT().
|
||||
DeleteMedia(gomock.Any(), 1, 1).
|
||||
Return(errors.New("failed to delete"))
|
||||
},
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
expectedError: "Failed to delete media",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
// 设置 mock
|
||||
tc.setupMock()
|
||||
|
||||
// 创建请求
|
||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/media/%s", tc.mediaID), nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Extract ID from URL path
|
||||
parts := strings.Split(strings.Trim(req.URL.Path, "/"), "/")
|
||||
if len(parts) >= 2 {
|
||||
c.Params = []gin.Param{{Key: "id", Value: parts[1]}}
|
||||
}
|
||||
|
||||
// 设置用户ID(除了未授权的测试用例)
|
||||
if tc.expectedError != "Unauthorized" {
|
||||
c.Set("user_id", 1)
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
s.handler.DeleteMedia(c)
|
||||
|
||||
// 验证响应
|
||||
s.Equal(tc.expectedStatus, w.Code)
|
||||
if tc.expectedError != "" {
|
||||
var response map[string]string
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
s.NoError(err)
|
||||
s.Equal(tc.expectedError, response["error"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
611
backend/internal/handler/post_handler_test.go
Normal file
611
backend/internal/handler/post_handler_test.go
Normal file
|
@ -0,0 +1,611 @@
|
|||
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/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{}
|
||||
s.handler = NewHandler(cfg, s.service)
|
||||
|
||||
// Setup Gin router
|
||||
gin.SetMode(gin.TestMode)
|
||||
s.router = gin.New()
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue