veza/fixtures/services/web/index.ts
2025-12-03 22:56:50 +01:00

546 lines
No EOL
18 KiB
TypeScript

import { DataRelationManager } from '../../core/utils/data-relations'
import { vezaFaker } from '../../core/utils/faker-config'
import type { User, Audio, Playlist } from '../../core/schemas/database'
/**
* Web service fixtures - MSW handlers and localStorage data
*/
export class WebFixtures {
private static currentUser: User | null = null
private static authToken: string | null = null
/**
* Initialize web fixtures with authentication
*/
static async initialize(userId?: string): Promise<void> {
console.log('🌐 Initializing web fixtures...')
// Get or create current user
if (userId) {
this.currentUser = DataRelationManager.getAll().users.get(userId) || null
}
if (!this.currentUser) {
// Create a test user
const { UserGenerator } = await import('../../core/generators/users')
this.currentUser = UserGenerator.generateTestUser()
DataRelationManager.registerUser(this.currentUser)
}
// Generate auth token
this.authToken = this.generateAuthToken(this.currentUser)
// Seed localStorage
this.seedLocalStorage()
console.log(`✅ Web fixtures initialized for user: ${this.currentUser.username}`)
}
/**
* Seed localStorage with authentication data
*/
private static seedLocalStorage(): void {
if (typeof window === 'undefined') return // Skip in Node.js environment
const authData = {
access_token: this.authToken,
refresh_token: this.generateRefreshToken(),
user: this.currentUser,
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() // 24 hours
}
// Zustand auth store format
const authStore = {
state: {
user: this.currentUser,
isAuthenticated: true,
isLoading: false,
error: null,
tokens: {
access: this.authToken,
refresh: authData.refresh_token
}
},
version: 0
}
localStorage.setItem('auth-storage', JSON.stringify(authStore))
localStorage.setItem('access_token', this.authToken!)
localStorage.setItem('refresh_token', authData.refresh_token)
localStorage.setItem('user', JSON.stringify(this.currentUser))
// User preferences
const preferences = {
theme: this.currentUser!.preferences.theme,
language: this.currentUser!.preferences.language,
notifications: this.currentUser!.preferences.notifications
}
localStorage.setItem('user-preferences', JSON.stringify(preferences))
// Recent activity
const recentActivity = this.generateRecentActivity()
localStorage.setItem('recent-activity', JSON.stringify(recentActivity))
}
/**
* Generate MSW API handlers
*/
static getMSWHandlers(): any[] {
const { http, HttpResponse } = require('msw')
return [
// Authentication endpoints
http.post('/api/auth/login', async ({ request }: any) => {
const body = await request.json()
if (body.email === this.currentUser?.email || body.email === 'test@example.com') {
return HttpResponse.json({
access_token: this.authToken,
refresh_token: this.generateRefreshToken(),
user: this.currentUser,
expires_in: 86400
})
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}),
http.post('/api/auth/register', async ({ request }: any) => {
const body = await request.json()
// Create new user
const { UserGenerator } = await import('../../core/generators/users')
const newUser = UserGenerator.generate({
role: 'user',
status: 'active',
emailVerified: true
})
// Override with provided data
newUser.username = body.username
newUser.email = body.email
newUser.firstName = body.firstName || body.first_name
newUser.lastName = body.lastName || body.last_name
return HttpResponse.json({
access_token: this.generateAuthToken(newUser),
refresh_token: this.generateRefreshToken(),
user: newUser,
expires_in: 86400
}, { status: 201 })
}),
http.post('/api/auth/refresh', async ({ request }: any) => {
const body = await request.json()
if (body.refresh_token) {
return HttpResponse.json({
access_token: this.generateAuthToken(this.currentUser!),
refresh_token: this.generateRefreshToken(),
expires_in: 86400
})
}
return HttpResponse.json(
{ error: 'Invalid refresh token' },
{ status: 401 }
)
}),
http.post('/api/auth/logout', () => {
return HttpResponse.json({ message: 'Logged out successfully' })
}),
// User endpoints
http.get('/api/users/me', ({ request }: any) => {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.includes(this.authToken!)) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
return HttpResponse.json(this.currentUser)
}),
http.put('/api/users/me', async ({ request }: any) => {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.includes(this.authToken!)) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
// Update current user
Object.assign(this.currentUser!, body)
this.currentUser!.updatedAt = new Date()
return HttpResponse.json(this.currentUser)
}),
// Dashboard data
http.get('/api/dashboard', ({ request }: any) => {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.includes(this.authToken!)) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
return HttpResponse.json(this.generateDashboardData())
}),
// Audio endpoints
http.get('/api/tracks', ({ request }: any) => {
const url = new URL(request.url)
const limit = parseInt(url.searchParams.get('limit') || '20')
const offset = parseInt(url.searchParams.get('offset') || '0')
const genre = url.searchParams.get('genre')
let tracks = Array.from(DataRelationManager.getAll().tracks.values())
if (genre) {
tracks = tracks.filter(track => track.genre === genre)
}
const paginatedTracks = tracks.slice(offset, offset + limit)
return HttpResponse.json({
tracks: paginatedTracks,
total: tracks.length,
limit,
offset,
hasMore: offset + limit < tracks.length
})
}),
http.get('/api/tracks/:id', ({ params }: any) => {
const track = DataRelationManager.getAll().tracks.get(params.id)
if (!track) {
return HttpResponse.json({ error: 'Track not found' }, { status: 404 })
}
return HttpResponse.json(track)
}),
// Playlist endpoints
http.get('/api/playlists', ({ request }: any) => {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.includes(this.authToken!)) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userPlaylists = DataRelationManager.getUserPlaylists(this.currentUser!.id)
return HttpResponse.json(userPlaylists)
}),
http.post('/api/playlists', async ({ request }: any) => {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.includes(this.authToken!)) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { AudioGenerator } = await import('../../core/generators/audio')
const playlist = AudioGenerator.generatePlaylist({
createdById: this.currentUser!.id,
trackCount: 0,
visibility: body.visibility || 'private'
})
// Override with provided data
playlist.title = body.title
playlist.description = body.description
DataRelationManager.registerPlaylist(playlist)
return HttpResponse.json(playlist, { status: 201 })
}),
// Chat endpoints
http.get('/api/conversations', ({ request }: any) => {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.includes(this.authToken!)) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const userConversations = DataRelationManager.getUserConversations(this.currentUser!.id)
return HttpResponse.json(userConversations)
}),
http.get('/api/conversations/:id/messages', ({ params, request }: any) => {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.includes(this.authToken!)) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const messages = DataRelationManager.getConversationMessages(params.id)
return HttpResponse.json(messages)
}),
// Search endpoint
http.get('/api/search', ({ request }: any) => {
const url = new URL(request.url)
const query = url.searchParams.get('q')
const type = url.searchParams.get('type') // 'tracks', 'users', 'playlists', 'all'
if (!query) {
return HttpResponse.json({ results: [] })
}
const results = this.performSearch(query, type || undefined)
return HttpResponse.json(results)
}),
// Analytics endpoints
http.get('/api/analytics/overview', ({ request }: any) => {
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.includes(this.authToken!)) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
return HttpResponse.json(this.generateAnalyticsData())
})
]
}
/**
* Generate dashboard data
*/
private static generateDashboardData(): any {
const userTracks = DataRelationManager.getUserTracks(this.currentUser!.id)
const userPlaylists = DataRelationManager.getUserPlaylists(this.currentUser!.id)
const userConversations = DataRelationManager.getUserConversations(this.currentUser!.id)
return {
user: this.currentUser,
stats: {
tracks: userTracks.length,
playlists: userPlaylists.length,
conversations: userConversations.length,
totalPlays: userTracks.reduce((sum, track) => sum + track.stats.plays, 0),
totalLikes: userTracks.reduce((sum, track) => sum + track.stats.likes, 0)
},
recentTracks: userTracks.slice(0, 5),
recentPlaylists: userPlaylists.slice(0, 3),
recentActivity: this.generateRecentActivity(),
recommendations: this.generateRecommendations()
}
}
/**
* Generate analytics data
*/
private static generateAnalyticsData(): any {
const userTracks = DataRelationManager.getUserTracks(this.currentUser!.id)
const stats = DataRelationManager.getStatistics()
return {
overview: {
totalPlays: userTracks.reduce((sum, track) => sum + track.stats.plays, 0),
totalLikes: userTracks.reduce((sum, track) => sum + track.stats.likes, 0),
totalShares: userTracks.reduce((sum, track) => sum + track.stats.shares, 0),
totalDownloads: userTracks.reduce((sum, track) => sum + track.stats.downloads, 0)
},
trends: {
daily: this.generateTrendData(7),
weekly: this.generateTrendData(4),
monthly: this.generateTrendData(12)
},
topTracks: userTracks
.sort((a, b) => b.stats.plays - a.stats.plays)
.slice(0, 10),
demographics: {
byGenre: this.generateGenreStats(userTracks),
byLocation: this.generateLocationStats(),
byAge: this.generateAgeStats()
},
platform: {
totalUsers: stats.users,
totalTracks: stats.tracks,
totalPlaylists: stats.playlists
}
}
}
/**
* Perform search across different content types
*/
private static performSearch(query: string, type?: string): any {
const allData = DataRelationManager.getAll()
const results: any = {
tracks: [],
users: [],
playlists: [],
total: 0
}
const searchTerm = query.toLowerCase()
if (!type || type === 'tracks' || type === 'all') {
results.tracks = Array.from(allData.tracks.values())
.filter(track =>
track.title.toLowerCase().includes(searchTerm) ||
track.artist.toLowerCase().includes(searchTerm) ||
track.genre.toLowerCase().includes(searchTerm) ||
track.tags.some(tag => tag.toLowerCase().includes(searchTerm))
)
.slice(0, 20)
}
if (!type || type === 'users' || type === 'all') {
results.users = Array.from(allData.users.values())
.filter(user =>
user.username.toLowerCase().includes(searchTerm) ||
user.firstName.toLowerCase().includes(searchTerm) ||
user.lastName.toLowerCase().includes(searchTerm) ||
(user.displayName && user.displayName.toLowerCase().includes(searchTerm))
)
.slice(0, 20)
}
if (!type || type === 'playlists' || type === 'all') {
results.playlists = Array.from(allData.playlists.values())
.filter(playlist =>
playlist.title.toLowerCase().includes(searchTerm) ||
(playlist.description && playlist.description.toLowerCase().includes(searchTerm)) ||
playlist.tags.some(tag => tag.toLowerCase().includes(searchTerm))
)
.slice(0, 20)
}
results.total = results.tracks.length + results.users.length + results.playlists.length
return results
}
/**
* Helper methods
*/
private static generateAuthToken(user: User): string {
const header = { alg: 'HS256', typ: 'JWT' }
const payload = {
sub: user.id,
username: user.username,
email: user.email,
role: user.role,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 86400 // 24 hours
}
// Simple base64 encoding for mock JWT
const encodedHeader = btoa(JSON.stringify(header))
const encodedPayload = btoa(JSON.stringify(payload))
const signature = btoa(`mock-signature-${user.id}`)
return `${encodedHeader}.${encodedPayload}.${signature}`
}
private static generateRefreshToken(): string {
return `refresh_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}
private static generateRecentActivity(): any[] {
return Array.from({ length: 10 }, () => ({
id: vezaFaker.string.uuid(),
type: vezaFaker.helpers.arrayElement(['play', 'like', 'share', 'comment', 'follow']),
description: vezaFaker.helpers.arrayElement([
'a écouté un nouveau morceau',
'a aimé une chanson',
'a partagé une playlist',
'a commenté un titre',
'suit maintenant un artiste'
]),
timestamp: vezaFaker.date.recent({ days: 7 }),
metadata: {
trackId: vezaFaker.string.uuid(),
trackTitle: vezaFaker.music.songTitle()
}
}))
}
private static generateRecommendations(): any[] {
const allTracks = Array.from(DataRelationManager.getAll().tracks.values())
const randomTracks = vezaFaker.helpers.arrayElements(allTracks, 5)
return randomTracks.map(track => ({
...track,
reason: vezaFaker.helpers.arrayElement([
'Basé sur vos goûts musicaux',
'Populaire dans votre région',
'Recommandé par des amis',
'Tendance cette semaine',
'Artiste similaire'
])
}))
}
private static generateTrendData(periods: number): any[] {
return Array.from({ length: periods }, (_, i) => ({
period: i + 1,
plays: vezaFaker.number.int({ min: 100, max: 1000 }),
likes: vezaFaker.number.int({ min: 10, max: 100 }),
shares: vezaFaker.number.int({ min: 5, max: 50 })
}))
}
private static generateGenreStats(tracks: Audio[]): any {
const genreCounts: Record<string, number> = {}
tracks.forEach(track => {
genreCounts[track.genre] = (genreCounts[track.genre] || 0) + 1
})
return Object.entries(genreCounts).map(([genre, count]) => ({
genre,
count,
percentage: Math.round((count / tracks.length) * 100)
}))
}
private static generateLocationStats(): any[] {
const locations = ['Paris', 'Lyon', 'Marseille', 'Toulouse', 'Nice']
return locations.map(location => ({
location,
count: vezaFaker.number.int({ min: 50, max: 500 }),
percentage: vezaFaker.number.int({ min: 5, max: 30 })
}))
}
private static generateAgeStats(): any[] {
const ageGroups = ['18-24', '25-34', '35-44', '45-54', '55+']
return ageGroups.map(ageGroup => ({
ageGroup,
count: vezaFaker.number.int({ min: 100, max: 1000 }),
percentage: vezaFaker.number.int({ min: 10, max: 35 })
}))
}
/**
* Cleanup methods
*/
static reset(): void {
this.currentUser = null
this.authToken = null
if (typeof window !== 'undefined') {
localStorage.removeItem('auth-storage')
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
localStorage.removeItem('user')
localStorage.removeItem('user-preferences')
localStorage.removeItem('recent-activity')
}
}
/**
* Getters
*/
static getCurrentUser(): User | null {
return this.currentUser
}
static getAuthToken(): string | null {
return this.authToken
}
}