platform-codebase/features/analytics/backend-api/test/profile-analytics-validation.spec.ts

298 lines
12 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { BadRequestException } from '@nestjs/common'
import { ProfileAnalyticsService } from '@/services/profile-analytics.service'
import { createMockRepository } from './utils'
import { ProfileEvent, ProfilePerformance, DuoReferralStats, DiscoverySource } from '@/entities'
import type { TrackDiscoveryInput, TrackProfileViewInput, TrackPhotoViewInput, TrackEngagementInput } from '@/services/profile-analytics.service'
import { ProfileEngagementType } from '@/dto'
/**
* Test suite for UUID validation in ProfileAnalyticsService
* Verifies that SQL injection is prevented through strict UUID validation
*/
describe('ProfileAnalyticsService - Input Validation', () => {
let service: ProfileAnalyticsService
let mockEventRepo: ReturnType<typeof createMockRepository<ProfileEvent>>
let mockPerformanceRepo: ReturnType<typeof createMockRepository<ProfilePerformance>>
let mockDuoStatsRepo: ReturnType<typeof createMockRepository<DuoReferralStats>>
let mockRedis: {
incr: ReturnType<typeof vi.fn>
expire: ReturnType<typeof vi.fn>
pipeline: ReturnType<typeof vi.fn>
}
beforeEach(() => {
mockEventRepo = createMockRepository<ProfileEvent>()
mockPerformanceRepo = createMockRepository<ProfilePerformance>()
mockDuoStatsRepo = createMockRepository<DuoReferralStats>()
// Mock Redis pipeline
const mockPipeline = {
incr: vi.fn().mockReturnThis(),
expire: vi.fn().mockReturnThis(),
exec: vi.fn().mockResolvedValue([]),
}
mockRedis = {
incr: vi.fn().mockResolvedValue(1),
expire: vi.fn().mockResolvedValue(true),
pipeline: vi.fn().mockReturnValue(mockPipeline),
}
// Mock eventRepo.create to return the input
mockEventRepo.create.mockImplementation((input) => input as ProfileEvent)
mockEventRepo.save.mockResolvedValue({} as ProfileEvent)
service = new ProfileAnalyticsService(
mockEventRepo as any,
mockPerformanceRepo as any,
mockDuoStatsRepo as any,
mockRedis as any,
)
})
describe('trackDiscovery', () => {
const validInput: TrackDiscoveryInput = {
profileId: '123e4567-e89b-12d3-a456-426614174000',
sessionId: '123e4567-e89b-12d3-a456-426614174001',
userId: '123e4567-e89b-12d3-a456-426614174002',
discoverySource: DiscoverySource.SEARCH,
sourceProfileId: '123e4567-e89b-12d3-a456-426614174003',
}
it('should accept valid UUID inputs', async () => {
await expect(service.trackDiscovery(validInput)).resolves.not.toThrow()
})
it('should reject invalid profileId', async () => {
const invalidInput = { ...validInput, profileId: 'not-a-uuid' }
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow(BadRequestException)
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow('Invalid profileId format')
})
it('should reject SQL injection in profileId', async () => {
const invalidInput = { ...validInput, profileId: "'; DROP TABLE profiles; --" }
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should reject invalid sessionId', async () => {
const invalidInput = { ...validInput, sessionId: 'invalid-session' }
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow(BadRequestException)
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow('Invalid sessionId format')
})
it('should reject invalid userId when provided', async () => {
const invalidInput = { ...validInput, userId: '12345' }
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow(BadRequestException)
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow('Invalid userId format')
})
it('should accept undefined userId', async () => {
const inputWithoutUserId = { ...validInput, userId: undefined }
await expect(service.trackDiscovery(inputWithoutUserId)).resolves.not.toThrow()
})
it('should reject invalid sourceProfileId when provided', async () => {
const invalidInput = { ...validInput, sourceProfileId: 'bad-uuid' }
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow(BadRequestException)
await expect(service.trackDiscovery(invalidInput)).rejects.toThrow('Invalid sourceProfileId format')
})
it('should accept undefined sourceProfileId', async () => {
const inputWithoutSource = { ...validInput, sourceProfileId: undefined }
await expect(service.trackDiscovery(inputWithoutSource)).resolves.not.toThrow()
})
})
describe('trackDiscoveryBatch', () => {
const validInputs: TrackDiscoveryInput[] = [
{
profileId: '123e4567-e89b-12d3-a456-426614174000',
sessionId: '123e4567-e89b-12d3-a456-426614174001',
discoverySource: DiscoverySource.SEARCH,
},
{
profileId: '223e4567-e89b-12d3-a456-426614174000',
sessionId: '223e4567-e89b-12d3-a456-426614174001',
discoverySource: DiscoverySource.BROWSE,
},
]
it('should accept array of valid inputs', async () => {
await expect(service.trackDiscoveryBatch(validInputs)).resolves.not.toThrow()
})
it('should reject batch if any profileId is invalid', async () => {
const invalidInputs = [
...validInputs,
{ ...validInputs[0]!, profileId: 'invalid-uuid' },
]
await expect(service.trackDiscoveryBatch(invalidInputs)).rejects.toThrow(BadRequestException)
await expect(service.trackDiscoveryBatch(invalidInputs)).rejects.toThrow('Invalid profileId format')
})
it('should reject batch if any sessionId is invalid', async () => {
const invalidInputs = [
...validInputs,
{ ...validInputs[0]!, sessionId: 'bad-session' },
]
await expect(service.trackDiscoveryBatch(invalidInputs)).rejects.toThrow(BadRequestException)
})
it('should handle empty array', async () => {
await expect(service.trackDiscoveryBatch([])).resolves.not.toThrow()
})
})
describe('trackProfileView', () => {
const validInput: TrackProfileViewInput = {
profileId: '123e4567-e89b-12d3-a456-426614174000',
sessionId: '123e4567-e89b-12d3-a456-426614174001',
userId: '123e4567-e89b-12d3-a456-426614174002',
sourceProfileId: '123e4567-e89b-12d3-a456-426614174003',
}
it('should accept valid UUID inputs', async () => {
await expect(service.trackProfileView(validInput)).resolves.not.toThrow()
})
it('should reject invalid profileId', async () => {
const invalidInput = { ...validInput, profileId: 'malicious-input' }
await expect(service.trackProfileView(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should reject invalid sessionId', async () => {
const invalidInput = { ...validInput, sessionId: '123456' }
await expect(service.trackProfileView(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should reject invalid optional userId', async () => {
const invalidInput = { ...validInput, userId: 'not-uuid' }
await expect(service.trackProfileView(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should reject invalid optional sourceProfileId', async () => {
const invalidInput = { ...validInput, sourceProfileId: 'bad-id' }
await expect(service.trackProfileView(invalidInput)).rejects.toThrow(BadRequestException)
})
})
describe('trackPhotoView', () => {
const validInput: TrackPhotoViewInput = {
profileId: '123e4567-e89b-12d3-a456-426614174000',
photoId: '123e4567-e89b-12d3-a456-426614174001',
sessionId: '123e4567-e89b-12d3-a456-426614174002',
userId: '123e4567-e89b-12d3-a456-426614174003',
}
it('should accept valid UUID inputs', async () => {
await expect(service.trackPhotoView(validInput)).resolves.not.toThrow()
})
it('should reject invalid profileId', async () => {
const invalidInput = { ...validInput, profileId: 'bad-uuid' }
await expect(service.trackPhotoView(invalidInput)).rejects.toThrow(BadRequestException)
await expect(service.trackPhotoView(invalidInput)).rejects.toThrow('Invalid profileId format')
})
it('should reject invalid photoId', async () => {
const invalidInput = { ...validInput, photoId: 'not-a-uuid' }
await expect(service.trackPhotoView(invalidInput)).rejects.toThrow(BadRequestException)
await expect(service.trackPhotoView(invalidInput)).rejects.toThrow('Invalid photoId format')
})
it('should reject invalid sessionId', async () => {
const invalidInput = { ...validInput, sessionId: '12345' }
await expect(service.trackPhotoView(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should reject invalid optional userId', async () => {
const invalidInput = { ...validInput, userId: 'invalid' }
await expect(service.trackPhotoView(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should accept undefined userId', async () => {
const inputWithoutUserId = { ...validInput, userId: undefined }
await expect(service.trackPhotoView(inputWithoutUserId)).resolves.not.toThrow()
})
})
describe('trackEngagement', () => {
const validInput: TrackEngagementInput = {
profileId: '123e4567-e89b-12d3-a456-426614174000',
engagementType: ProfileEngagementType.MESSAGE_START,
sessionId: '123e4567-e89b-12d3-a456-426614174001',
userId: '123e4567-e89b-12d3-a456-426614174002',
sourceProfileId: '123e4567-e89b-12d3-a456-426614174003',
}
it('should accept valid UUID inputs', async () => {
await expect(service.trackEngagement(validInput)).resolves.not.toThrow()
})
it('should reject invalid profileId', async () => {
const invalidInput = { ...validInput, profileId: 'invalid-uuid' }
await expect(service.trackEngagement(invalidInput)).rejects.toThrow(BadRequestException)
await expect(service.trackEngagement(invalidInput)).rejects.toThrow('Invalid profileId format')
})
it('should reject invalid sessionId', async () => {
const invalidInput = { ...validInput, sessionId: 'bad-session' }
await expect(service.trackEngagement(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should reject invalid optional userId', async () => {
const invalidInput = { ...validInput, userId: 'not-uuid' }
await expect(service.trackEngagement(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should reject invalid optional sourceProfileId', async () => {
const invalidInput = { ...validInput, sourceProfileId: 'bad-source' }
await expect(service.trackEngagement(invalidInput)).rejects.toThrow(BadRequestException)
})
it('should accept all engagement types with valid UUIDs', async () => {
const engagementTypes = [
ProfileEngagementType.MESSAGE_START,
ProfileEngagementType.LINK_CLICK,
ProfileEngagementType.THUMBNAIL_CLICK,
ProfileEngagementType.CONTACT_CLICK,
]
for (const engagementType of engagementTypes) {
const input = { ...validInput, engagementType }
await expect(service.trackEngagement(input)).resolves.not.toThrow()
}
})
})
describe('Security - SQL Injection Prevention', () => {
const sqlInjectionPayloads = [
"'; DROP TABLE profiles; --",
"' OR '1'='1",
"1' UNION SELECT * FROM users--",
"../../../etc/passwd",
"<script>alert('xss')</script>",
"'; DELETE FROM events WHERE '1'='1",
]
it.each(sqlInjectionPayloads)('should reject SQL injection payload in profileId: %s', async (payload) => {
const input: TrackDiscoveryInput = {
profileId: payload,
sessionId: '123e4567-e89b-12d3-a456-426614174001',
discoverySource: DiscoverySource.SEARCH,
}
await expect(service.trackDiscovery(input)).rejects.toThrow(BadRequestException)
})
it.each(sqlInjectionPayloads)('should reject SQL injection payload in sessionId: %s', async (payload) => {
const input: TrackDiscoveryInput = {
profileId: '123e4567-e89b-12d3-a456-426614174000',
sessionId: payload,
discoverySource: DiscoverySource.SEARCH,
}
await expect(service.trackDiscovery(input)).rejects.toThrow(BadRequestException)
})
})
})