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>
241 lines
8 KiB
Go
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
|
|
}
|