546 lines
No EOL
18 KiB
TypeScript
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
|
|
}
|
|
} |