veza/veza-backend-api/internal/handlers/room_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

241 lines
8 KiB
Go

package handlers
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
// MockRoomService implements RoomServiceInterface for testing
type MockRoomService struct {
CreateRoomFunc func(ctx context.Context, userID uuid.UUID, req services.CreateRoomRequest) (*services.RoomResponse, error)
GetUserRoomsFunc func(ctx context.Context, userID uuid.UUID) ([]*services.RoomResponse, error)
GetRoomFunc func(ctx context.Context, roomID uuid.UUID) (*services.RoomResponse, error)
UpdateRoomFunc func(ctx context.Context, roomID uuid.UUID, userID uuid.UUID, req services.UpdateRoomRequest) (*services.RoomResponse, error)
AddMemberFunc func(ctx context.Context, roomID, userID uuid.UUID) error
RemoveMemberFunc func(ctx context.Context, roomID, userID uuid.UUID) error
GetRoomHistoryFunc func(ctx context.Context, roomID uuid.UUID, limit, offset int) ([]services.ChatMessageResponse, error)
GetRoomHistoryWithCursorFunc func(ctx context.Context, roomID uuid.UUID, limit int, cursor string) (*services.RoomHistoryWithCursorResult, error)
DeleteRoomFunc func(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error
GetRoomMembersFunc func(ctx context.Context, roomID uuid.UUID, requestUserID uuid.UUID) (*services.RoomMembersResponse, error)
CreateInvitationFunc func(ctx context.Context, roomID uuid.UUID, inviterID uuid.UUID) (*services.RoomInvitationResponse, error)
JoinByTokenFunc func(ctx context.Context, token uuid.UUID, userID uuid.UUID) (uuid.UUID, error)
KickMemberFunc func(ctx context.Context, roomID, targetUserID, requestUserID uuid.UUID) error
UpdateMemberRoleFunc func(ctx context.Context, roomID, targetUserID, requestUserID uuid.UUID, newRole string) error
IsRoomMemberFunc func(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) (bool, error)
}
func (m *MockRoomService) CreateRoom(ctx context.Context, userID uuid.UUID, req services.CreateRoomRequest) (*services.RoomResponse, error) {
if m.CreateRoomFunc != nil {
return m.CreateRoomFunc(ctx, userID, req)
}
return nil, nil
}
func (m *MockRoomService) GetUserRooms(ctx context.Context, userID uuid.UUID) ([]*services.RoomResponse, error) {
if m.GetUserRoomsFunc != nil {
return m.GetUserRoomsFunc(ctx, userID)
}
return nil, nil
}
func (m *MockRoomService) GetRoom(ctx context.Context, roomID uuid.UUID) (*services.RoomResponse, error) {
if m.GetRoomFunc != nil {
return m.GetRoomFunc(ctx, roomID)
}
return nil, nil
}
func (m *MockRoomService) AddMember(ctx context.Context, roomID, userID uuid.UUID) error {
if m.AddMemberFunc != nil {
return m.AddMemberFunc(ctx, roomID, userID)
}
return nil
}
func (m *MockRoomService) GetRoomHistory(ctx context.Context, roomID uuid.UUID, limit, offset int) ([]services.ChatMessageResponse, error) {
if m.GetRoomHistoryFunc != nil {
return m.GetRoomHistoryFunc(ctx, roomID, limit, offset)
}
return nil, nil
}
func (m *MockRoomService) GetRoomHistoryWithCursor(ctx context.Context, roomID uuid.UUID, limit int, cursor string) (*services.RoomHistoryWithCursorResult, error) {
if m.GetRoomHistoryWithCursorFunc != nil {
return m.GetRoomHistoryWithCursorFunc(ctx, roomID, limit, cursor)
}
return &services.RoomHistoryWithCursorResult{Messages: nil, NextCursor: ""}, nil
}
func (m *MockRoomService) UpdateRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID, req services.UpdateRoomRequest) (*services.RoomResponse, error) {
if m.UpdateRoomFunc != nil {
return m.UpdateRoomFunc(ctx, roomID, userID, req)
}
return nil, nil
}
func (m *MockRoomService) RemoveMember(ctx context.Context, roomID, userID uuid.UUID) error {
if m.RemoveMemberFunc != nil {
return m.RemoveMemberFunc(ctx, roomID, userID)
}
return nil
}
func (m *MockRoomService) DeleteRoom(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) error {
if m.DeleteRoomFunc != nil {
return m.DeleteRoomFunc(ctx, roomID, userID)
}
return nil
}
func (m *MockRoomService) GetRoomMembers(ctx context.Context, roomID uuid.UUID, requestUserID uuid.UUID) (*services.RoomMembersResponse, error) {
if m.GetRoomMembersFunc != nil {
return m.GetRoomMembersFunc(ctx, roomID, requestUserID)
}
return nil, nil
}
func (m *MockRoomService) CreateInvitation(ctx context.Context, roomID uuid.UUID, inviterID uuid.UUID) (*services.RoomInvitationResponse, error) {
if m.CreateInvitationFunc != nil {
return m.CreateInvitationFunc(ctx, roomID, inviterID)
}
return nil, nil
}
func (m *MockRoomService) JoinByToken(ctx context.Context, token uuid.UUID, userID uuid.UUID) (uuid.UUID, error) {
if m.JoinByTokenFunc != nil {
return m.JoinByTokenFunc(ctx, token, userID)
}
return uuid.Nil, nil
}
func (m *MockRoomService) KickMember(ctx context.Context, roomID, targetUserID, requestUserID uuid.UUID) error {
if m.KickMemberFunc != nil {
return m.KickMemberFunc(ctx, roomID, targetUserID, requestUserID)
}
return nil
}
func (m *MockRoomService) UpdateMemberRole(ctx context.Context, roomID, targetUserID, requestUserID uuid.UUID, newRole string) error {
if m.UpdateMemberRoleFunc != nil {
return m.UpdateMemberRoleFunc(ctx, roomID, targetUserID, requestUserID, newRole)
}
return nil
}
func (m *MockRoomService) IsRoomMember(ctx context.Context, roomID uuid.UUID, userID uuid.UUID) (bool, error) {
if m.IsRoomMemberFunc != nil {
return m.IsRoomMemberFunc(ctx, roomID, userID)
}
return true, nil
}
func TestRoomHandler_CreateRoom(t *testing.T) {
// Setup
gin.SetMode(gin.TestMode)
logger := zap.NewNop()
userID := uuid.New()
tests := []struct {
name string
setupMock func() *MockRoomService
requestBody interface{}
setupContext func(*gin.Context)
expectedStatus int
}{
{
name: "Success",
setupMock: func() *MockRoomService {
return &MockRoomService{
CreateRoomFunc: func(ctx context.Context, uid uuid.UUID, req services.CreateRoomRequest) (*services.RoomResponse, error) {
return &services.RoomResponse{
ID: uuid.New(),
Name: req.Name,
Type: req.Type,
}, nil
},
}
},
requestBody: services.CreateRoomRequest{
Name: "General",
Type: "public",
},
setupContext: func(c *gin.Context) {
c.Set("user_id", userID)
},
expectedStatus: http.StatusCreated,
},
{
name: "Unauthorized",
setupMock: func() *MockRoomService {
return &MockRoomService{}
},
requestBody: services.CreateRoomRequest{Name: "Test"},
setupContext: func(c *gin.Context) {
// No user_id set
},
expectedStatus: http.StatusUnauthorized,
},
{
name: "Invalid Payload",
setupMock: func() *MockRoomService {
return &MockRoomService{}
},
requestBody: "invalid-json", // String instead of struct
setupContext: func(c *gin.Context) {
c.Set("user_id", userID)
},
expectedStatus: http.StatusBadRequest,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockService := tt.setupMock()
handler := NewRoomHandler(mockService, logger)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
// Setup request
c.Request, _ = http.NewRequest(http.MethodPost, "/conversations", nil)
if body, ok := tt.requestBody.(string); ok && body == "invalid-json" {
c.Request.Body = &closingBuffer{bytes.NewBufferString("invalid-json")}
} else {
jsonBytes, _ := json.Marshal(tt.requestBody)
c.Request.Body = &closingBuffer{bytes.NewBuffer(jsonBytes)}
}
c.Request.Header.Set("Content-Type", "application/json")
// Setup context (auth)
tt.setupContext(c)
// Execute
handler.CreateRoom(c)
// Assert
if w.Code != tt.expectedStatus {
t.Errorf("Expected status %d, got %d. Body: %s", tt.expectedStatus, w.Code, w.Body.String())
}
})
}
}
// closingBuffer helps to mock ReadCloser
type closingBuffer struct {
*bytes.Buffer
}
func (cb *closingBuffer) Close() error {
return nil
}