veza/veza-backend-api/internal/handlers/chat_handler_test.go
senke 24b29d229d fix(v0.12.6.1): remediate 2 CRITICAL + 10 HIGH + 1 MEDIUM pentest findings
Security fixes implemented:

CRITICAL:
- CRIT-001: IDOR on chat rooms — added IsRoomMember check before
  returning room data or message history (returns 404, not 403)
- CRIT-002: play_count/like_count exposed publicly — changed JSON
  tags to "-" so they are never serialized in API responses

HIGH:
- HIGH-001: TOCTOU race on marketplace downloads — transaction +
  SELECT FOR UPDATE on GetDownloadURL
- HIGH-002: HS256 in production docker-compose — replaced JWT_SECRET
  with JWT_PRIVATE_KEY_PATH / JWT_PUBLIC_KEY_PATH (RS256)
- HIGH-003: context.Background() bypass in user repository — full
  context propagation from handlers → services → repository (29 files)
- HIGH-004: Race condition on promo codes — SELECT FOR UPDATE
- HIGH-005: Race condition on exclusive licenses — SELECT FOR UPDATE
- HIGH-006: Rate limiter IP spoofing — SetTrustedProxies(nil) default
- HIGH-007: RGPD hard delete incomplete — added cleanup for sessions,
  settings, follows, notifications, audit_logs anonymization
- HIGH-008: RTMP callback auth weak — fail-closed when unconfigured,
  header-only (no query param), constant-time compare
- HIGH-009: Co-listening host hijack — UpdateHostState now takes *Conn
  and verifies IsHost before processing
- HIGH-010: Moderator self-strike — added issuedBy != userID check

MEDIUM:
- MEDIUM-001: Recovery codes used math/rand — replaced with crypto/rand
- MEDIUM-005: Stream token forgeable — resolved by HIGH-002 (RS256)

Updated REMEDIATION_MATRIX: 14 findings marked  CORRIGÉ.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 05:40:53 +01:00

225 lines
6.2 KiB
Go

package handlers
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.uber.org/zap"
)
// MockChatServiceForChatHandler mocks ChatService
type MockChatServiceForChatHandler struct {
mock.Mock
}
func (m *MockChatServiceForChatHandler) GenerateToken(userID uuid.UUID, username string) (*services.ChatTokenResponse, error) {
args := m.Called(userID, username)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*services.ChatTokenResponse), args.Error(1)
}
func (m *MockChatServiceForChatHandler) GetStats(ctx context.Context) (*services.ChatStats, error) {
args := m.Called(ctx)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*services.ChatStats), args.Error(1)
}
// MockUserServiceForChatHandler mocks UserService
type MockUserServiceForChatHandler struct {
mock.Mock
}
func (m *MockUserServiceForChatHandler) GetByID(_ context.Context, userID uuid.UUID) (*models.User, error) {
args := m.Called(userID)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.User), args.Error(1)
}
func setupTestChatRouter(mockChatService *MockChatServiceForChatHandler, mockUserService *MockUserServiceForChatHandler) *gin.Engine {
gin.SetMode(gin.TestMode)
router := gin.New()
logger := zap.NewNop()
handler := NewChatHandlerWithInterface(mockChatService, mockUserService, logger)
api := router.Group("/api/v1/chat")
api.Use(func(c *gin.Context) {
userIDStr := c.GetHeader("X-User-ID")
if userIDStr != "" {
uid, err := uuid.Parse(userIDStr)
if err == nil {
c.Set("user_id", uid)
}
}
c.Next()
})
{
api.GET("/token", handler.GetToken)
api.GET("/stats", handler.GetStats)
}
return router
}
func TestChatHandler_GetToken_Success(t *testing.T) {
// Setup
mockChatService := new(MockChatServiceForChatHandler)
mockUserService := new(MockUserServiceForChatHandler)
router := setupTestChatRouter(mockChatService, mockUserService)
userID := uuid.New()
username := "testuser"
expectedToken := &services.ChatTokenResponse{
Token: "test-token-123",
}
user := &models.User{
ID: userID,
Username: username,
}
mockUserService.On("GetByID", userID).Return(user, nil)
mockChatService.On("GenerateToken", userID, username).Return(expectedToken, nil)
// Execute
req, _ := http.NewRequest("GET", "/api/v1/chat/token", nil)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
mockChatService.AssertExpectations(t)
mockUserService.AssertExpectations(t)
}
func TestChatHandler_GetToken_Unauthorized(t *testing.T) {
// Setup
mockChatService := new(MockChatServiceForChatHandler)
mockUserService := new(MockUserServiceForChatHandler)
router := setupTestChatRouter(mockChatService, mockUserService)
// Execute - No X-User-ID header
req, _ := http.NewRequest("GET", "/api/v1/chat/token", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusUnauthorized, w.Code)
mockChatService.AssertNotCalled(t, "GenerateToken")
mockUserService.AssertNotCalled(t, "GetByID")
}
func TestChatHandler_GetToken_UserServiceError(t *testing.T) {
// Setup
mockChatService := new(MockChatServiceForChatHandler)
mockUserService := new(MockUserServiceForChatHandler)
router := setupTestChatRouter(mockChatService, mockUserService)
userID := uuid.New()
expectedToken := &services.ChatTokenResponse{
Token: "test-token-123",
}
// UserService returns error, handler should use fallback username
mockUserService.On("GetByID", userID).Return(nil, assert.AnError)
mockChatService.On("GenerateToken", userID, mock.MatchedBy(func(username string) bool {
return username == "user_"+userID.String()
})).Return(expectedToken, nil)
// Execute
req, _ := http.NewRequest("GET", "/api/v1/chat/token", nil)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
mockChatService.AssertExpectations(t)
mockUserService.AssertExpectations(t)
}
func TestChatHandler_GetToken_ChatServiceError(t *testing.T) {
// Setup
mockChatService := new(MockChatServiceForChatHandler)
mockUserService := new(MockUserServiceForChatHandler)
router := setupTestChatRouter(mockChatService, mockUserService)
userID := uuid.New()
username := "testuser"
user := &models.User{
ID: userID,
Username: username,
}
mockUserService.On("GetByID", userID).Return(user, nil)
mockChatService.On("GenerateToken", userID, username).Return(nil, assert.AnError)
// Execute
req, _ := http.NewRequest("GET", "/api/v1/chat/token", nil)
req.Header.Set("X-User-ID", userID.String())
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusInternalServerError, w.Code)
mockChatService.AssertExpectations(t)
mockUserService.AssertExpectations(t)
}
func TestChatHandler_GetStats_Success(t *testing.T) {
// Setup
mockChatService := new(MockChatServiceForChatHandler)
mockUserService := new(MockUserServiceForChatHandler)
router := setupTestChatRouter(mockChatService, mockUserService)
expectedStats := &services.ChatStats{
TotalMessages: 100,
ActiveUsers: 10,
}
mockChatService.On("GetStats", mock.Anything).Return(expectedStats, nil)
// Execute
req, _ := http.NewRequest("GET", "/api/v1/chat/stats", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusOK, w.Code)
mockChatService.AssertExpectations(t)
}
func TestChatHandler_GetStats_ServiceError(t *testing.T) {
// Setup
mockChatService := new(MockChatServiceForChatHandler)
mockUserService := new(MockUserServiceForChatHandler)
router := setupTestChatRouter(mockChatService, mockUserService)
mockChatService.On("GetStats", mock.Anything).Return(nil, assert.AnError)
// Execute
req, _ := http.NewRequest("GET", "/api/v1/chat/stats", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
// Assert
assert.Equal(t, http.StatusInternalServerError, w.Code)
mockChatService.AssertExpectations(t)
}