chore: consolidate CI, E2E, backend and frontend updates

- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
This commit is contained in:
senke 2026-02-17 16:43:21 +01:00
parent 6ab03bf3df
commit e11984898d
271 changed files with 18178 additions and 1714 deletions

View file

@ -48,7 +48,7 @@ jobs:
docker build -t veza-stream-server:${{ github.sha }} .
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: 'veza-backend-api:${{ github.sha }}'
format: 'table'
@ -56,7 +56,7 @@ jobs:
severity: 'CRITICAL,HIGH'
- name: Trivy scan frontend
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: 'veza-frontend:${{ github.sha }}'
format: 'table'
@ -64,7 +64,7 @@ jobs:
severity: 'CRITICAL,HIGH'
- name: Trivy scan chat server
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: 'veza-chat-server:${{ github.sha }}'
format: 'table'
@ -72,7 +72,7 @@ jobs:
severity: 'CRITICAL,HIGH'
- name: Trivy scan stream server
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: 'veza-stream-server:${{ github.sha }}'
format: 'table'
@ -111,23 +111,26 @@ jobs:
if: ${{ secrets.DOCKER_REGISTRY != '' && secrets.COSIGN_PRIVATE_KEY != '' }}
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
run: |
echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > cosign.key
for svc in veza-backend-api veza-frontend veza-chat-server veza-stream-server; do
cosign sign --key cosign.key --yes "${{ secrets.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}"
cosign sign --key cosign.key --yes "${{ secrets.DOCKER_REGISTRY }}/${svc}:latest"
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ secrets.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}"
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${{ secrets.DOCKER_REGISTRY }}/${svc}:latest"
done
- name: Deploy to Kubernetes
if: ${{ secrets.KUBE_CONFIG != '' }}
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > ~/.kube/config
KUBECONFIG="${{ runner.temp }}/kubeconfig"
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$KUBECONFIG"
chmod 600 "$KUBECONFIG"
export KUBECONFIG
for svc in veza-backend-api veza-chat-server veza-stream-server; do
kubectl set image "deployment/${svc}" "${svc}=${{ secrets.DOCKER_REGISTRY }}/${svc}:${{ github.sha }}" \
-n veza --record || echo "Skipping ${svc} (deployment not found)"
done
kubectl rollout status deployment/veza-backend-api -n veza --timeout=300s || true
rm -f "$KUBECONFIG"
- name: Deployment Summary
run: |

View file

@ -262,6 +262,7 @@ jobs:
CORS_ALLOWED_ORIGINS: http://veza.fr:5173,http://veza.fr:5174,http://localhost:5173,http://localhost:5174
RABBITMQ_URL: amqp://veza:devpassword@localhost:15672/
DISABLE_RATE_LIMIT_FOR_TESTS: "true"
ACCOUNT_LOCKOUT_EXEMPT_EMAILS: "e2e@test.com"
run: |
cd veza-backend-api
go build -o veza-api ./cmd/api/main.go

View file

@ -1,30 +0,0 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

1
.gitignore vendored
View file

@ -108,6 +108,7 @@ config/incus/env/*.env
# Playwright
/test-results/
/playwright-report/
apps/web/e2e-results.json
/blob-report/
/playwright/.cache/
/playwright/.auth/

View file

@ -0,0 +1,370 @@
# Rapport d'état précis des features — Veza
**Date** : 16 février 2026
**Méthode** : Analyse du code source (backend routes, frontend services, migrations DB, tests)
---
## 1. CARTOGRAPHIE GLOBALE — ÉTAT PRÉCIS
### 1.1 Stack
| Couche | Technologie | Version | Fichiers clés |
|--------|-------------|---------|---------------|
| Frontend | React + Vite | 18.2 / 7.1.5 | `apps/web/package.json` |
| Backend | Go + Gin | 1.24 / 1.11 | `veza-backend-api/go.mod` |
| Chat | Rust + Axum | 0.8 | `veza-chat-server/Cargo.toml` |
| Stream | Rust + Axum | 0.8 | `veza-stream-server/Cargo.toml` |
| DB | PostgreSQL | 16 | `docker-compose.prod.yml` |
| Cache | Redis | 7 | idem |
| Queue | RabbitMQ | 3 | idem |
### 1.2 Organisation du repo
- **apps/web** : Frontend React (features/, services/, mocks/)
- **veza-backend-api** : API REST (router principal : `internal/api/router.go`)
- **veza-chat-server** : WebSocket chat
- **veza-stream-server** : Streaming audio
- **veza-common** : Lib Rust partagée
- **packages/** : NPM packages partagés
### 1.3 Point d'entrée API
Le routeur **actif** est `APIRouter` dans `internal/api/router.go`.
Le fichier `api_manager.go` est **exclu de la compilation** (`//go:build ignore`) — tout ce qu'il contient (achievements, leaderboard, GraphQL, gRPC, etc.) est du **code mort**.
---
## 2. ÉTAT PRÉCIS DE CHAQUE FEATURE
### 2.1 Auth (register, login, JWT, refresh)
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ Complet | `routes_auth.go` : register, login, refresh, logout, /me, 2FA, OAuth, password reset |
| Frontend | ✅ Complet | `authStore`, `LoginForm`, `TwoFactorVerify`, `ProtectedRoute` |
| DB | ✅ | Tables users, sessions, refresh_tokens, email_verification_tokens |
| Tests | ✅ | `auth_handler_test.go`, `auth_integration_test.go`, `LoginForm.stories` |
| Sécurité | ✅ | JWT iss/aud/exp, token version, bcrypt cost 12, rate limit login |
**Verdict** : **Opérationnel**
---
### 2.2 2FA (TOTP)
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `TwoFactorHandler` : setup, verify, disable, status |
| Frontend | ✅ | `TwoFactorSetup.tsx`, `TwoFactorVerify.tsx` |
| DB | ✅ | Colonnes two_factor_enabled, two_factor_secret, backup_codes |
| Tests | ✅ | `two_factor_handler_test.go` |
**Verdict** : **Opérationnel**
---
### 2.3 OAuth (Google, GitHub, Discord)
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `OAuthHandler` : providers, initiate, callback |
| Frontend | ✅ | Boutons OAuth, callback handling |
| DB | ✅ | oauth_accounts, users |
**Verdict** : **Opérationnel**
---
### 2.4 Profils utilisateur
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_users.go` : GET/PUT/DELETE /users/:id, settings, avatar, follow, block |
| Frontend | ✅ | `ProfileView`, `ProfilePage`, `useUser` |
| DB | ✅ | users, user_profiles, user_settings |
**Verdict** : **Opérationnel**
---
### 2.5 Upload de tracks (chunked)
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_tracks.go` : initiate, chunk, complete, resume, quota |
| Frontend | ✅ | `trackService`, upload flow |
| DB | ✅ | tracks, track_uploads |
| Sécurité | ✅ | RequireContentCreatorRole, ClamAV optionnel |
**Verdict** : **Opérationnel**
---
### 2.6 CRUD Tracks
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | GET/PUT/DELETE tracks, comments, likes, share, versions, play |
| Frontend | ✅ | `trackService`, `LibraryPage`, `TrackDetailPage` |
| DB | ✅ | tracks, track_comments, track_likes |
**Verdict** : **Opérationnel**
---
### 2.7 Playlists (CRUD, collaboration)
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_playlists.go` : CRUD, collaborators, tracks |
| Frontend | ✅ | `playlistService`, `PlaylistDetailPage` |
| DB | ✅ | playlists, playlist_collaborators, playlist_tracks |
**Verdict** : **Opérationnel**
---
### 2.8 Chat WebSocket
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_chat.go` : POST /chat/token, GET /chat/stats |
| Chat Server | ✅ | Rust, compile OK |
| Frontend | ✅ | `ChatView`, WebSocket client |
| DB | ✅ | chat_messages (Chat Server) |
**Verdict** : **Opérationnel** (Chat Server doit être démarré)
---
### 2.9 Dashboard
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_core.go:319` : GET /dashboard, `DashboardHandler` |
| Frontend | ✅ | `dashboardService.getDashboardData()` → apiClient.get('/dashboard') |
| MSW | ✅ | Mock dans `handlers-admin.ts` (fallback Storybook) |
**Note** : FEATURE_STATUS.md indiquait "MSW" — **faux**. Le backend expose bien `/api/v1/dashboard`.
**Verdict** : **Opérationnel**
---
### 2.10 Recherche
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `TrackSearchService`, endpoints search |
| Frontend | ✅ | `SearchPage`, `searchService` |
**Verdict** : **Opérationnel**
---
### 2.11 Social (feed, posts, groups, follows, blocks)
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_social.go` : feed, posts, groups, like, comments, join/leave |
| Frontend | ✅ | `SocialView`, `useSocialView` |
| DB | ✅ | posts, social_groups, user_follows, user_blocks |
**Verdict** : **Opérationnel**
---
### 2.12 Administration
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_core.go` : admin group, RequireAdmin |
| Frontend | ✅ | `AdminDashboardPage`, `adminService` |
| Audit | ✅ | audit/logs, audit/stats |
**Verdict** : **Opérationnel**
---
### 2.13 Marketplace
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_marketplace.go` : products, cart, orders, licenses |
| Frontend | ✅ | `MarketplacePage`, `Cart`, `PurchasesView` |
| Paiement | ✅ | Hyperswitch intégré |
| DB | ✅ | marketplace_products, orders, licenses |
**Verdict** : **Opérationnel**
---
### 2.14 Webhooks
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_webhooks.go` : CRUD, regenerate-key, test, stats |
| Frontend | ✅ | `webhookService.ts` (apiClient), `WebhooksView` |
| DB | ✅ | webhooks |
**Note** : `webhookApi.ts` supprimé — remplacé par `webhookService.ts` qui appelle l'API directement.
**Verdict** : **Opérationnel**
---
### 2.15 Inventory / Gear
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_gear.go` : GET/POST/PUT/DELETE /inventory/gear |
| Frontend | ✅ | `gearService.ts`, `GearView`, `GearPage` |
| DB | ✅ | Migration 076 : `gear_items` |
| MSW | ✅ | Mock dans `handlers-misc.ts` (Storybook) |
**Note** : FEATURE_STATUS.md indiquait "UI + mocks, pas de backend" — **faux**. Backend complet.
**Verdict** : **Opérationnel**
---
### 2.16 Live Streaming
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_live.go` : GET /live/streams, GET /live/streams/:id, POST (auth) |
| Frontend | ✅ | `liveService.ts`, `LiveView`, `LivePage` |
| DB | ✅ | Migration 077 : `live_streams` |
| MSW | ✅ | Mock dans `handlers-misc.ts` |
**Note** : Le streaming vidéo réel (WebRTC/HLS) est géré par le Stream Server. Les routes backend gèrent les **métadonnées** des streams (titre, description, is_live).
**Verdict** : **Opérationnel** (métadonnées). Stream vidéo dépend du Stream Server.
---
### 2.17 Analytics
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_analytics.go` : tracks plays, top, dashboard |
| Frontend | ✅ | `AnalyticsView`, `useAnalyticsView` |
| DB | ✅ | track_plays, analytics events |
**Verdict** : **Opérationnel**
---
### 2.18 Roles
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `setupRoleRoutes` : assign, revoke |
| Frontend | ✅ | `AssignRoleModal`, `RolesPage` |
| DB | ✅ | roles, user_roles |
**Verdict** : **Opérationnel**
---
### 2.19 Notifications
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ✅ | `routes_core.go` : GET/POST/DELETE /api/v1/notifications, unread-count, read, read-all. Création auto pour follow, like, comment (Phase 2.2) |
| Frontend | ✅ | `NotificationsPage`, `notificationService` |
| DB | ✅ | Table `notifications` (migration 047) |
**Verdict** : **Opérationnel**
---
### 2.20 Gamification (achievements, leaderboard)
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ❌ Code mort | `api_manager.go` (build ignore) : handleGetAchievements, handleGetLeaderboard |
| Frontend | ⚠️ Composants | Storybook : AchievementCard, LeaderboardView, XPBar — pas de route /gamification |
| MSW | ? | Handlers gamification possibles dans mocks |
**Verdict** : **Fantôme** — api_manager désactivé, pas de route active
---
### 2.21 Studio (Cloud File Browser)
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ❌ | Aucune route |
| Frontend | ❌ | Dossier `features/studio/` **n'existe pas** (supprimé) |
**Verdict** : **Supprimé**
---
### 2.22 Education
| Aspect | État | Preuve |
|--------|------|--------|
| Backend | ❌ | Aucune route |
| Frontend | ❌ | Dossier `features/education/` **n'existe pas** (supprimé) |
**Verdict** : **Supprimé**
---
## 3. RÉCAPITULATIF
### Features opérationnelles (19)
Auth, 2FA, OAuth, Profils, Upload tracks, CRUD tracks, Playlists, Chat, Dashboard, Recherche, Social, Admin, Marketplace, Webhooks, Gear, Live (métadonnées), Analytics, Roles, Notifications.
### Features partielles (0)
Aucune.
### Features fantômes (1)
Gamification — code dans api_manager (mort), composants Storybook.
### Features supprimées (2)
Studio, Education — dossiers supprimés.
---
## 4. INCOHÉRENCES DOCUMENTATION / CODE
| Document | Affirmation | Réalité |
|----------|-------------|---------|
| FEATURE_STATUS.md | Dashboard : MSW | Backend réel GET /dashboard |
| FEATURE_STATUS.md | Inventory : pas de backend | Backend complet /inventory/gear |
| FEATURE_STATUS.md | Live : contenu minimal | Backend complet /live/streams |
| FEATURE_STATUS.md | Studio : UI seule | Dossier supprimé |
| FEATURE_STATUS.md | Education : MSW | Dossier supprimé |
**Recommandation** : Mettre à jour `docs/FEATURE_STATUS.md` et `apps/web/docs/FEATURE_STATUS.md`.
---
## 5. FICHIERS CRITIQUES PAR FEATURE
| Feature | Backend | Frontend service | Route |
|---------|---------|------------------|-------|
| Auth | routes_auth.go | authStore | /auth/* |
| Tracks | routes_tracks.go | trackService | /tracks/* |
| Playlists | routes_playlists.go | playlistService | /playlists/* |
| Chat | routes_chat.go | - | /chat/* |
| Dashboard | routes_core.go | dashboardService | /dashboard |
| Social | routes_social.go | - | /social/* |
| Marketplace | routes_marketplace.go | - | /marketplace/* |
| Webhooks | routes_webhooks.go | webhookService | /webhooks/* |
| Gear | routes_gear.go | gearService | /inventory/gear |
| Live | routes_live.go | liveService | /live/streams |
| Analytics | routes_analytics.go | - | /analytics/* |
| Roles | routes_users.go | - | /users/:id/roles |
---
*Rapport généré le 16 février 2026*

View file

@ -0,0 +1,737 @@
I now have all the data needed. Let me write the comprehensive audit report.
---
# 🔍 AUDIT COMPLET DU MONOREPO VEZA
**Date** : 16 février 2026
**Auditeur** : Architecte IA senior
**Scope** : Monorepo complet (`veza-backend-api`, `veza-chat-server`, `veza-stream-server`, `veza-common`, `apps/web`)
---
## PARTIE 1 — ÉTAT DE STABILITÉ
---
### 1.1 Santé du code
#### Go Backend (`veza-backend-api/`) ✅
| Critère | Statut | Détail |
|---------|--------|--------|
| Compilation (`go build ./...`) | ✅ Passe | 0 erreur, 0 warning |
| Vet (`go vet ./...`) | ✅ Passe | 0 issue |
| Imports cassés | ✅ Aucun | — |
| `.env.template` | ✅ Documenté | Complet avec validation rules |
| Secrets hardcodés | ✅ Aucun | Tous via env vars, masqués dans logs |
**TODOs/FIXMEs critiques (P1) — 7 items :**
| Fichier | Ligne | Description |
|---------|-------|-------------|
| `internal/core/track/handler.go` | ~340 | `TODO(P2-GO-004)`: `trackUploadService` attend `int64`, reçoit `uuid.UUID` — migration UUID incomplète |
| `internal/core/track/handler.go` | ~355 | `TODO(P2-GO-004)`: même problème, `GetUploadProgress()` incompatible UUID |
| `internal/repositories/playlist_collaborator_repository.go` | ~67 | `FIXME`: modèle `PlaylistCollaborator` doit utiliser UUID |
| `internal/services/playlist_version_service.go` | ~73 | `FIXME`: `PlaylistVersion` ID types à vérifier |
| `internal/services/track_history_service.go` | ~74 | `FIXME`: `TrackHistory` needs UUID migration |
| `internal/services/playlist_service.go` | ~216 | `FIXME`: `PlaylistVersionService` needs UUID update |
| `internal/handlers/auth_handler_test.go` | 225 | `FIXME`: test attend `StatusForbidden` mais l'implémentation permet login non-vérifié |
**TODOs P2 (18 items)** — les plus notables :
| Fichier | Description |
|---------|-------------|
| `internal/services/job_service.go` | Job queue non connectée (5 TODOs BE-SVC-003) — pas d'async processing |
| `internal/database/database.go` | OAuth user lookup non implémenté (3 TODOs) |
| `internal/handlers/oauth_handlers.go` | `frontendURL` fallback hardcodé `http://localhost:5173` |
| `internal/config/middlewares_init.go:75` | Configuration CORS à améliorer |
| `internal/api/admin/service.go` | Admin service partiellement implémenté (3 TODOs) |
#### Rust Chat Server (`veza-chat-server/`) ✅
| Critère | Statut | Détail |
|---------|--------|--------|
| Compilation (`cargo check`) | ✅ Passe | 0 erreur, 0 warning |
| Protobuf | ✅ | Utilise fichiers pré-générés |
| `.env.lab.example` | ⚠️ Minimal | Seul un template lab, pas de `.env.example` standard |
**TODOs (3 items) :**
- `src/read_receipts.rs:230` — TODO: tracking "delivered" non implémenté
- `src/presence.rs:226` — TODO: intégration push notifications (FCM, APNs)
- `src/message_handler.rs:327` — TODO: recherche de salon par nom
#### Rust Stream Server (`veza-stream-server/`) ✅
| Critère | Statut | Détail |
|---------|--------|--------|
| Compilation (`cargo check`) | ✅ Passe | 0 erreur, 0 warning |
| Protobuf | ✅ | Utilise fichiers pré-générés |
| `.env.example` | ✅ Documenté | Variables bien documentées |
| `#![allow(dead_code)]` | ⚠️ | Code mort autorisé dans `lib.rs` |
**Point critique** : le client gRPC vers le backend Go (`src/grpc/mod.rs`) est un **stub**`attempt_send()` fait juste un `sleep`, il n'envoie rien réellement.
#### Rust Common (`veza-common/`) ✅
| Critère | Statut |
|---------|--------|
| Compilation | ✅ Passe |
| TODOs | ✅ Aucun |
#### Frontend React (`apps/web/`) ✅
| Critère | Statut | Détail |
|---------|--------|--------|
| TypeScript (`tsc --noEmit`) | ✅ Passe | 0 erreur |
| Build Vite | ✅ Passe | — |
| `.env.example` | ✅ Documenté | Complet avec feature flags |
**TODOs notables :**
- `src/services/analyticsService.ts:92-97` — endpoints analytics non implémentés côté backend, retournent des valeurs vides
- `src/config/features.ts:50` — HLS endpoints marqués "NOT IMPLEMENTED"
- `src/features/user/components/profile/ProfileSecurity.tsx:12` — "Placeholder for profile security"
---
### 1.2 Points bloquants fonctionnels
| Module | Statut | Détail |
|--------|--------|--------|
| **Auth** | ✅ Fonctionnel | Register → verify email → login → refresh → logout → 2FA TOTP : flow complet. OAuth Google/GitHub opérationnel. Sessions management complet (list/revoke/logout-all). |
| **Profils** | ✅ Fonctionnel | Création, édition, avatar upload, profil public (`/u/:username`), social links, paramètres. Toutes les routes connectées frontend ↔ backend. |
| **Upload & Fichiers** | ⚠️ Partiel | Upload simple ✅, upload chunked ✅, validation MIME/taille ✅, métadonnées extraites ✅. **Manque** : transcoding async (job queue stub), HLS transcoding désactivé (feature flag `false`). |
| **Streaming/Lecteur** | ⚠️ Partiel | Play/pause/seek/next/volume/shuffle/repeat ✅ via `<audio>` HTML5. Waveform visualizer ✅. Queue management ✅. **Manque** : HLS adaptive streaming désactivé, gRPC stream server est un stub, crossfade/gapless non implémentés. |
| **Playlists** | ✅ Fonctionnel | CRUD complet ✅, ajout/retrait tracks ✅, réorganisation ✅, collaboration ✅, share links ✅, export JSON/CSV ✅, duplication ✅. |
| **Chat** | ⚠️ Partiel | WebSocket connection ✅, envoi/réception messages ✅, conversations ✅, typing indicators ✅, reactions ✅. **Manque** : read receipts partiels (TODO), delivered status (TODO), recherche salon par nom (TODO). Communication avec Go backend via HTTP (pas gRPC). |
| **Marketplace** | ✅ Fonctionnel | Création produit ✅, catalogue ✅, panier ✅, wishlist ✅, commandes ✅. Checkout via Hyperswitch (optionnel). Téléchargement post-achat ✅. |
| **Recherche** | ✅ Fonctionnel | Recherche globale tracks/users/playlists ✅, autocomplete ✅. Filtres par type ✅. |
---
### 1.3 Points bloquants techniques
#### Base de données ⚠️
- **42 migrations** bien structurées, idempotentes, avec `IF NOT EXISTS`
- **Migration UUID incomplète** : 6 FIXMEs dans le backend indiquent que certains services (`trackUploadService`, `PlaylistCollaborator`, `PlaylistVersion`, `TrackHistory`) utilisent encore `int64` au lieu de `uuid.UUID`. Cela compile (Go est permissif avec les conversions) mais peut causer des bugs runtime.
- Pas de conflits de migrations détectés
#### API — Routes orphelines ⚠️
**Backend non consommé par le frontend :**
- `POST /api/v1/tracks/initiate` (chunked upload initiate) — frontend utilise directement `/tracks/chunk`
- `POST /api/v1/tracks/complete` (chunked upload complete) — même remarque
- `GET /api/v1/tracks/resume/:uploadId` — pas de UI de reprise d'upload
- `POST /api/v1/tracks/batch/delete` et `POST /api/v1/tracks/batch/update` — pas de UI batch
- `GET /api/v1/tracks/shared/:token` — pas de page de partage par token
- `GET /api/v1/users/me/export` — endpoint existe, pas de bouton export dans l'UI
- `POST /api/v1/audit/cleanup` — pas d'UI admin pour cleanup
**Frontend appelle des endpoints qui n'existent pas côté backend :**
- `POST /api/v1/roles` (création de rôle) — le backend n'a que `GET /roles` et `GET /roles/:id`
- `PUT /api/v1/roles/:id`, `DELETE /api/v1/roles/:id` — idem
- `GET /api/v1/social/feed`, `POST /api/v1/social/posts` — pas de routes social dans le backend (uniquement follow/block)
- `GET /api/v1/social/groups/*` — pas de routes groupes dans le backend
- `GET /api/v1/inventory/gear/*` — pas de routes inventaire dans le backend
- `GET /api/v1/live/streams/*` — pas de routes live dans le backend
- `GET /api/v1/search` — le backend utilise `/tracks/search`, `/users/search`, pas un endpoint unifié `/search`
#### Sécurité ✅
- JWT correctement validé via middleware auth
- CORS configuré (origines spécifiques, pas de wildcard)
- CSRF protection via middleware + tokens
- Security headers complets (HSTS, CSP, X-Frame-Options, X-Content-Type-Options)
- Rate limiting multi-couche (global, par endpoint, par utilisateur)
- SQL injection protection (GORM parameterized queries)
- Secret masking dans les logs
- Aucun secret hardcodé en production (seuls des fallbacks dev dans le code)
#### Services Rust ⚠️
- **Compilation** : ✅ Les deux compilent sans erreur
- **Dépendances Cargo** : ✅ Résolues
- **Communication avec Go** : 🔴 Le stream server utilise un **stub gRPC**`attempt_send()` ne fait qu'un `sleep`. Le chat server communique via HTTP vers le backend Go (fonctionnel mais pas gRPC comme prévu).
#### Docker ✅
- `docker-compose.yml` bien structuré : Postgres 16, Redis 7, RabbitMQ 3, backend-api, Hyperswitch (optionnel)
- Health checks sur tous les services
- Resource limits configurés
- Ports isolés (15xxx/16xxx pour éviter les conflits)
- Fichiers Dockerfile dev et production pour chaque service
#### Frontend — Tests ⚠️
**Tests unitaires (Vitest)** :
- **271/273 fichiers passent** (99.3%)
- **3306/3318 tests passent** (99.6%)
- **2 fichiers échouent** :
1. `src/features/tracks/components/LikeButton.test.tsx` — 11 tests en échec : `aria-label` attend `"Retirer le like"` mais reçoit `"Retirer des favoris"` (problème de label i18n)
2. `src/context/ToastContext.test.tsx` — 1 test en échec : `TypeError: (0, default) is not a function` dans `ToastProvider.tsx:40` (import cassé de `react-hot-toast`)
**Tests E2E (Playwright)** :
- Dernière exécution : **36 tests échoués** (sur un nombre indéterminé — la dernière run a échoué en setup à cause d'un conflit de port 5173)
- Configuration : 4 browsers (Chromium, Firefox, WebKit, Edge), 1 worker, timeout 60s
#### Logs & Observabilité ✅
- Logging structuré : `zap` (Go), `tracing` (Rust)
- Prometheus metrics sur tous les services
- Sentry integration (Go backend, frontend)
- Health checks : `/health`, `/healthz`, `/readyz`, `/api/v1/status`
- Health check détaillé vérifie : DB, Redis, RabbitMQ, S3, chat server, stream server
- Audit logs complets avec recherche
---
### 1.4 Synthèse stabilité
```
PRIORITÉ CRITIQUE (bloque le lancement) :
1. gRPC Stream Server stub — Le stream server ne communique pas réellement avec
le backend Go, la chaîne upload→transcode→stream est cassée.
Fichier: veza-stream-server/src/grpc/mod.rs
Effort: 8h
2. Routes API frontend ↔ backend désalignées — Le frontend appelle des endpoints
inexistants (/social/feed, /social/groups, /inventory/gear, /live/streams, /search).
Ces pages fonctionnent uniquement grâce aux mocks MSW.
Fichiers: apps/web/src/services/socialService.ts, gearService.ts, liveService.ts, searchService.ts
Effort: 16h (créer les routes backend) ou 4h (retirer les pages du routeur)
3. Job Queue non connectée — Les tâches async (transcoding, email, thumbnails) ne
s'exécutent pas en background. Le service existe mais est un shell vide.
Fichier: veza-backend-api/internal/services/job_service.go
Effort: 8h
PRIORITÉ HAUTE (dégrade l'expérience) :
1. Migration UUID incomplète — 6 services utilisent encore int64, risque de bugs
runtime sur upload progress, playlist collaborators, track history.
Fichiers: internal/core/track/handler.go:340, internal/services/playlist_*.go,
internal/repositories/playlist_collaborator_repository.go
Effort: 6h
2. HLS Streaming désactivé — Le lecteur audio ne supporte que le playback direct
(pas d'adaptive bitrate). Feature flag HLS_STREAMING=false.
Fichiers: apps/web/src/config/features.ts, veza-stream-server/
Effort: 12h
3. Tests LikeButton et ToastContext cassés — 12 tests unitaires échouent.
Fichiers: apps/web/src/features/tracks/components/LikeButton.test.tsx,
apps/web/src/context/ToastContext.test.tsx
Effort: 1h
4. Tests E2E non fiables — 36 échecs, configuration port conflict.
Fichier: apps/web/playwright.config.ts (reuseExistingServer: false)
Effort: 4h
PRIORITÉ MOYENNE (acceptable pour un PoC) :
1. Chat read receipts et delivered status — TODOs non implémentés
Fichiers: veza-chat-server/src/read_receipts.rs, src/delivered_status.rs
Effort: 4h
2. OAuth Discord/Spotify non implémentés — Seuls Google et GitHub fonctionnent
Fichiers: veza-backend-api/internal/handlers/oauth_handlers.go
Effort: 4h par provider
3. Admin service partiellement implémenté (3 TODOs)
Fichier: veza-backend-api/internal/api/admin/service.go
Effort: 4h
4. Analytics backend partiellement stub — Certains endpoints retournent des données vides
Fichier: apps/web/src/services/analyticsService.ts:92-97
Effort: 6h
5. Studio et Education supprimés — Features planifiées mais code retiré
Impact: Aucun pour le PoC (Tier 2)
Effort: 0h (décision produit)
```
---
## PARTIE 2 — PROGRESSION VERS L'OBJECTIF FINAL (600 FEATURES)
---
### 2.1 Matrice de couverture par module
> **Note** : Le document TIER 0 mentionne "40 features" mais les ranges listées (`1-10, 31-45, 66-90, 106-135, 151-175, 186-200, 226-250, 351-365, 411-425, 436-450`) contiennent en réalité **190 features**. J'utilise les ranges comme référence.
---
## Module 1 : Auth & Sécurité — 18/30 features (60%)
### Implémentées ✅ (backend + frontend connectés) :
- #1 : Inscription email/password ✅
- #2 : Validation email ✅
- #3 : Connexion email/password ✅
- #4 : OAuth Google ✅
- #5 : OAuth GitHub ✅
- #9 : Logout ✅
- #10 : Logout all devices ✅
- #11 : Reset password par email ✅
- #17 : Blocage après tentatives (rate limiting) ✅
- #19 : 2FA TOTP ✅
- #23 : Session management ✅
- #28 : Rate limiting connexion ✅
### Partiellement implémentées ⚠️ :
- #8 : Remember me ⚠️ — Cookies persistent mais pas de checkbox UI explicite
- #12 : Changement password (authentifié) ⚠️ — Endpoint frontend existe, backend probablement aussi
- #14 : Force du mot de passe ⚠️ — Validation Zod côté frontend, indicateur visuel partiel
- #21 : Codes backup 2FA ⚠️ — Modèle `recovery_code.go` existe, UI incomplète
- #26 : Historique connexions ⚠️ — Via audit logs, pas de page dédiée
- #30 : Détection bruteforce ⚠️ — Via rate limiting, pas de détection spécifique
### Non implémentées ❌ :
- #6 : OAuth Discord ❌
- #7 : OAuth Spotify ❌
- #13 : Historique passwords ❌
- #15 : Politique passwords configurable ❌
- #16 : Expiration password ❌
- #18 : Notification changement password ❌
- #20 : 2FA SMS ❌
- #22 : Passkeys/WebAuthn ❌
- #24 : Notifications connexion inhabituelle ❌
- #25 : Géolocalisation connexions ❌
- #27 : IP whitelisting ❌
- #29 : CAPTCHA ❌
---
## Module 2 : Profils & Utilisateurs — 18/35 features (51%)
### Implémentées ✅ :
- #31 : Avatar upload ✅
- #33 : Username unique ✅
- #34 : Nom complet ✅
- #35 : Bio/description ✅
- #39 : Langue préférée ✅
- #41 : URL profil (/u/username) ✅
- #44 : Liens réseaux sociaux ✅
- #46 : Rôle User ✅
- #47 : Rôle Artist ✅
- #51 : Rôle Modérateur ✅
- #52 : Rôle Admin ✅
- #53 : Permissions granulaires ✅
- #58 : Changement langue UI ✅
- #59 : Thème clair/sombre/auto ✅
- #65 : Supprimer compte (GDPR) ✅
### Partiellement implémentées ⚠️ :
- #32 : Bannière profil ⚠️ — Modèle existe probablement, pas de route dédiée
- #36 : Localisation ⚠️ — Champ probable dans user model
- #42 : Profil public/privé ⚠️ — Paramètres de confidentialité existent
- #56 : Changer email ⚠️ — Endpoint probable
- #57 : Changer username ⚠️ — Via PUT /users/:id
- #60-62 : Notifications on/off ⚠️ — Paramètres existent, implémentation partielle
- #63-64 : Préférences confidentialité/visibilité ⚠️ — Settings partiels
### Non implémentées ❌ :
- #37 : Date de naissance ❌
- #38 : Genre ❌
- #40 : Fuseau horaire ❌
- #43 : Email contact public ❌
- #45 : Badges/achievements ❌
- #48 : Rôle Producer ❌ (distinct d'Artist)
- #49 : Rôle Label ❌
- #50 : Rôle Formateur ❌
- #54 : Système vérification (badge vérifié) ❌
- #55 : KYC ❌
---
## Module 3 : Gestion de Fichiers — 14/40 features (35%)
### Implémentées ✅ :
- #66 : Upload fichier unique ✅
- #67 : Upload multiple (batch) ✅
- #71 : Progress bar upload ✅
- #73 : Validation taille ✅
- #74 : Validation type MIME ✅
- #79 : Extraction métadonnées ✅
- #81-86 : Formats MP3, WAV, FLAC, OGG, AIFF, M4A ✅
- #91-94 : Titre, Artiste, Album, Genre ✅
- #97 : Durée ✅
- #103 : Cover art upload ✅
- #104 : Tags personnalisés ✅
### Partiellement implémentées ⚠️ :
- #68 : Drag & drop ⚠️ — Probable via composant upload
- #72 : Pause/resume upload ⚠️ — Chunked upload existe mais UI incomplète
- #77 : Transcoding auto ⚠️ — Job queue stub, transcoding pipeline Rust existe mais non connecté
- #95 : BPM ⚠️ — Modèle existe, extraction auto incertaine
- #96 : Key musicale ⚠️ — Idem
- #98 : Date de sortie ⚠️ — Champ métadonnée probable
- #105 : Tags suggérés ⚠️ — Autocomplete partiel
### Non implémentées ❌ :
- #69 : Upload par URL ❌
- #70 : Upload depuis cloud (Dropbox/Drive) ❌
- #75 : Scan antivirus ❌ (ClamAV configuré mais `ENABLE_CLAMAV=false`)
- #76 : Compression auto images ❌
- #78 : Thumbnails auto ❌ (job queue stub)
- #80 : Watermarking ❌
- #87-88 : Archives ZIP/RAR ❌
- #89 : Documents PDF ❌
- #90 : Presets VST ❌
- #99-102 : Label, ISRC, Copyright, Lyrics ❌
---
## Module 4 : Streaming Audio — 16/45 features (36%)
### Implémentées ✅ :
- #106 : Play/pause ✅
- #107 : Next track ✅
- #108 : Previous track ✅
- #109 : Seek ✅
- #110 : Volume control ✅
- #111 : Mute/unmute ✅
- #112 : Shuffle ✅
- #113 : Repeat (off/track/playlist) ✅
- #117 : Waveform visualizer ✅
- #122 : Raccourcis clavier ✅ (Media Session API)
- #126 : Queue management ✅
- #127 : Ajouter à la queue ✅
- #128 : Retirer de la queue ✅
- #131 : Vider la queue ✅
- #136 : Créer playlist ✅
- #137 : Éditer playlist ✅
### Partiellement implémentées ⚠️ :
- #120 : Mini-player ⚠️ — Lecteur bottom-bar existe
- #123 : Media Session API ⚠️ — Probable via composant player
- #129 : Réorganiser queue ⚠️ — Store support, UI incertaine
- #132 : Historique écoute ⚠️ — Backend endpoint existe, UI partielle
- #133 : Reprendre où on s'est arrêté ⚠️ — playerStore persiste avec zustand persist
### Non implémentées ❌ :
- #114 : Playback speed ❌
- #115 : Crossfade ❌
- #116 : Gapless playback ❌
- #118 : Spectrogram ❌
- #119 : Bars visualizer ❌
- #121 : Picture-in-picture ❌
- #124 : Chromecast ❌
- #125 : AirPlay ❌
- #130 : Sauvegarder queue comme playlist ❌
- #134 : Queue collaborative ❌
- #135 : Autoplay recommandations ❌
- #138-150 : Playlists CRUD suite (la plupart implémentées — voir Playlists ci-dessus)
> **Correction Playlists** : Features 136-150 sont dans Module 4 mais le CRUD playlist est complet. En réalité : #136-142 ✅, #143 ✅ (collaboration), #144 ⚠️ (cover custom), #145 ✅ (description), #146 ✅ (partage), #147 ✅ (duplication), #148 ❌ (fusion), #149 ✅ (export), #150 ❌ (playlists intelligentes).
---
## Module 5 : Chat & Messagerie — 14/35 features (40%)
### Implémentées ✅ :
- #151 : DM 1-to-1 ✅
- #152 : Salons publics ✅
- #153 : Salons privés ✅
- #154 : Messages de groupe ✅
- #155 : Messages texte ✅
- #157 : Réactions emoji ✅
- #158 : Édition messages ✅
- #159 : Suppression messages ✅
- #170 : Notifications temps réel ✅
- #173 : Badge non lus ✅
- #174 : Typing indicator ✅
### Partiellement implémentées ⚠️ :
- #156 : Emojis ⚠️ — Texte emoji OK, pas de picker dédié
- #160 : Threads/réponses ⚠️ — Infrastructure existe dans le hub Rust
- #175 : Read receipts ⚠️ — Modèle existe, TODO dans le code
### Non implémentées ❌ :
- #161-165 : Mentions, Markdown, images, GIFs, partage tracks ❌
- #166-169 : Recherche historique, filtres, pin, bookmarks ❌
- #171-172 : Push notifications, son personnalisable ❌
- #176-185 : Présence & statuts (en ligne, occupé, custom, AFK, last seen, etc.) ❌
---
## Module 6 : Social & Communauté — 7/40 features (18%)
### Implémentées ✅ :
- #186 : Follow ✅
- #187 : Unfollow ✅
- #188 : Liste followers ✅ (endpoint existe)
- #189 : Liste following ✅
- #190 : Bloquer ✅
- #191 : Signaler ⚠️ (modération backend, pas de bouton frontend dédié)
### Partiellement implémentées ⚠️ :
- #196 : Partage profil ⚠️ — URL `/u/:username` existe
- #198 : Notifications followers ⚠️ — Notifications système existe
### Non implémentées ❌ :
- #192-195, 197, 199-200 : Recommandations, suggestions, collaboration, referral, QR code, close friends, abonnements ❌
- #201-225 : Mur & publications, groupes & communautés ❌ — Le frontend a des composants Social mais ils appellent des endpoints qui **n'existent pas** dans le backend (uniquement MSW mocks)
---
## Module 7 : Marketplace — 16/50 features (32%)
### Implémentées ✅ :
- #226 : Créer produit ✅
- #227 : Éditer produit ✅
- #228 : Supprimer produit ✅
- #229 : Upload fichiers produit ✅
- #233 : Prix fixe ✅
- #236 : Catégories ✅
- #237 : Tags ✅
- #251 : Ajouter au panier ✅
- #252 : Panier multi-produits ✅
- #253 : Wishlist ✅
- #261 : Historique achats ✅
- #262 : Re-téléchargement ✅
- #266 : Dashboard vendeur ✅
### Partiellement implémentées ⚠️ :
- #230 : Preview/démo ⚠️ — Upload existe, player intégré incertain
- #232 : Description rich text ⚠️
- #256 : Checkout (Hyperswitch) ⚠️ — Infrastructure existe, optionnel
### Non implémentées ❌ :
- #231, 234-235, 238-250, 254-260, 263-275 : Images multi, prix variable, gratuit, BPM/Key, formats, licences complètes, paiements avancés, factures, remboursements, revenus temps réel, reviews, promotions, payout ❌
---
## Module 8 : Formation & Éducation — 0/30 features (0%)
**Entièrement non implémenté**. Le répertoire `src/features/education/` a été supprimé. Aucun code backend ne supporte ce module.
---
## Module 9 : Gestion de Matériel — 0/25 features (0%)
⚠️ Le frontend a des composants via MSW mocks (`/api/v1/inventory/gear`), mais **aucun endpoint backend n'existe**. Code frontend-only, non fonctionnel sans mocks.
---
## Module 10 : Cloud & Stockage — 0/20 features (0%)
**Entièrement non implémenté**. Aucune intégration Nextcloud ou backup.
---
## Module 11 : Recherche & Découverte — 6/30 features (20%)
### Implémentées ✅ :
- #351 : Recherche fulltext ✅
- #353 : Recherche tracks ✅
- #357 : Recherche utilisateurs ✅
- #356 : Recherche playlists ✅
- #360 : Autocomplete suggestions ✅
### Partiellement implémentées ⚠️ :
- #352 : Recherche par catégorie ⚠️ — Filtres existent
- #373 : Tri par pertinence ⚠️
### Non implémentées ❌ :
- #354-355, 358-359, 361-380 : Albums, groupes, cours, phonétique, correction ortho, booléen, historique, recherches sauvées, filtres avancés (BPM, key, durée), recommandations ❌
---
## Module 12 : Analytics & Statistiques — 5/30 features (17%)
### Implémentées ✅ :
- #381 : Dashboard analytics ✅
- #383 : Plays par track ✅
### Partiellement implémentées ⚠️ :
- #382 : Statistiques écoute globales ⚠️ — Endpoints partiels, certains retournent des données vides
- #393 : Engagement (likes, comments, shares) ⚠️
- #406 : Utilisateurs actifs (admin) ⚠️ — Admin dashboard partiel
### Non implémentées ❌ :
- #384-392, 394-405, 407-410 : Plays par période, durée moyenne, skip rate, géographie, démographie, devices, sources trafic, peaks, export, revenus, conversions, projections ❌
---
## Module 13 : Administration — 8/25 features (32%)
### Implémentées ✅ :
- #411 : Liste utilisateurs ✅
- #412 : Recherche utilisateurs ✅
- #418 : Changement de rôle ✅
- #419 : Historique actions admin ✅ (audit logs)
- #431 : Paramètres généraux ⚠️ (partiel)
- #433 : Feature flags ✅
### Partiellement implémentées ⚠️ :
- #413 : Filtres avancés ⚠️
- #432 : Limites upload/storage ⚠️ — Configurable via env
### Non implémentées ❌ :
- #414-417, 420-430, 434-435 : Édition profil admin, ban, suspension, reset password, notes internes, modération contenu, copyright, appeal, maintenance mode, annonces ❌
---
## Module 14 : UX/UI — 8/20 features (40%)
### Implémentées ✅ :
- #436 : Thème clair ✅
- #437 : Thème sombre ✅
- #438 : Thème auto ✅
- #446 : Navigation clavier ✅
- #448 : ARIA labels ✅ (partiellement — l'erreur LikeButton montre une incohérence)
- #449 : Focus visible ✅
- #452 : Réduction animations ✅ (prefers-reduced-motion supporté par Framer Motion)
### Partiellement implémentées ⚠️ :
- #450 : Contraste WCAG AA ⚠️ — Design system existe, conformité non auditée
### Non implémentées ❌ :
- #439-445, 447, 451, 453-455 : Contraste élevé, mode compact/confortable, couleurs custom, layouts custom, screen reader complet, tailles police, transcriptions, sous-titres, dyslexie ❌
---
## Modules 15-21 : Fonctionnalités Avancées 🔮
| Module | Features | Implémenté | Statut |
|--------|----------|------------|--------|
| 15. IA & Avancé | 45 | 0 | 🔮 Futur — Aucun code |
| 16. Intégrations | 20 | 0 | 🔮 Futur — Aucun code |
| 17. Apps Natives | 15 | 0 | 🔮 Futur — veza-mobile abandonné |
| 18. Gamification | 15 | 0 | 🔮 Futur — MSW mocks uniquement |
| 19. Notifications | 20 | 5 ⚠️ | ⚠️ Notifications in-app partielles (#551-555) |
| 20. Sécurité Avancée | 15 | 10 ✅ | ✅ Rate limiting, CSRF, XSS, CSP, HSTS, security headers (#571-580), audit logs (#581) |
| 21. Développeurs & API | 15 | 4 ⚠️ | ⚠️ API REST partielle (#586), Swagger (#591), Webhooks (#595), Developer dashboard UI only (#600) |
---
### 2.2 Tableau récapitulatif
| Module | Total | ✅ Done | ⚠️ Partiel | ❌ Missing | 🔮 Future | % Done |
|-------------------------------|-------|---------|------------|-----------|-----------|--------|
| 1. Auth & Sécurité | 30 | 12 | 6 | 12 | 0 | 40% |
| 2. Profils & Utilisateurs | 35 | 15 | 7 | 13 | 0 | 43% |
| 3. Gestion de Fichiers | 40 | 14 | 7 | 19 | 0 | 35% |
| 4. Streaming Audio | 45 | 24 | 5 | 16 | 0 | 53% |
| 5. Chat & Messagerie | 35 | 11 | 3 | 21 | 0 | 31% |
| 6. Social & Communauté | 40 | 5 | 2 | 33 | 0 | 13% |
| 7. Marketplace | 50 | 13 | 3 | 34 | 0 | 26% |
| 8. Formation & Éducation | 30 | 0 | 0 | 0 | 30 | 0% |
| 9. Gestion Matériel | 25 | 0 | 0 | 0 | 25 | 0% |
| 10. Cloud & Stockage | 20 | 0 | 0 | 0 | 20 | 0% |
| 11. Recherche & Découverte | 30 | 5 | 2 | 23 | 0 | 17% |
| 12. Analytics & Statistiques | 30 | 2 | 3 | 25 | 0 | 7% |
| 13. Administration | 25 | 6 | 2 | 17 | 0 | 24% |
| 14. UX/UI | 20 | 7 | 1 | 12 | 0 | 35% |
| 15. Fonctionnalités Avancées | 45 | 0 | 0 | 0 | 45 | 0% |
| 16. Intégrations Externes | 20 | 0 | 0 | 0 | 20 | 0% |
| 17. Applications Natives | 15 | 0 | 0 | 0 | 15 | 0% |
| 18. Gamification | 15 | 0 | 0 | 0 | 15 | 0% |
| 19. Notifications | 20 | 3 | 2 | 5 | 10 | 15% |
| 20. Sécurité Avancée | 15 | 10 | 1 | 0 | 4 | 67% |
| 21. Développeurs & API | 15 | 2 | 2 | 6 | 5 | 13% |
| **TOTAL** | **600** | **129** | **46** | **236** | **189** | **21.5%** |
---
### 2.3 Écart par rapport aux tiers de priorité
#### TIER 0 (V1 Launch — ranges 1-10, 31-45, 66-90, 106-135, 151-175, 186-200, 226-250, 351-365, 411-425, 436-450 = ~190 features)
| Sous-range | Total | ✅ | ⚠️ | ❌ | % |
|------------|-------|-----|------|------|-----|
| Auth 1-10 | 10 | 7 | 1 | 2 | 70% |
| Profils 31-45 | 15 | 10 | 3 | 2 | 67% |
| Fichiers 66-90 | 25 | 10 | 3 | 12 | 40% |
| Streaming 106-135 | 30 | 14 | 4 | 12 | 47% |
| Chat 151-175 | 25 | 11 | 3 | 11 | 44% |
| Social 186-200 | 15 | 5 | 2 | 8 | 33% |
| Marketplace 226-250 | 25 | 10 | 2 | 13 | 40% |
| Recherche 351-365 | 15 | 5 | 2 | 8 | 33% |
| Admin 411-425 | 15 | 4 | 1 | 10 | 27% |
| UX/UI 436-450 | 15 | 7 | 1 | 7 | 47% |
| **TOTAL TIER 0** | **190** | **83** | **22** | **85** | **44%** |
**Estimation effort pour finir TIER 0** : ~85 features manquantes dont beaucoup sont mineures (champs de formulaire, filtres). Estimation réaliste : **200-300h de développement** (6-10 semaines à temps plein).
#### TIER 1 (V2-V5 — ranges 11-30, 46-65, 91-105, 136-150, 176-185, 201-225, 251-275, 276-305, 306-330, 366-410 = ~230 features)
- **Déjà commencées** : ~36 features (2FA #19-21, rôles #46-53, playlists avancées #136-150 partiellement, rate limiting #28)
- Beaucoup de features TIER 1 sont déjà partiellement en place grâce au backend riche
#### TIER 2 (V6-V12 — features 426-435, 451-600 = ~160 features + modules 8-10 = ~75 = ~235 features)
- **Code anticipatoire** : Infrastructure Kubernetes complète (k8s/), monitoring Prometheus/Grafana, load testing scripts, security scanning CI — l'infra est surdimensionnée par rapport au code applicatif.
- Le modèle `live_stream.go` et les composants Live frontend anticipent le livestreaming (#471-480)
- Les modèles `gear.go`, `hardware.go` anticipent l'inventaire (#306-330)
- Les modèles `contest.go`, `royalty.go` anticipent la gamification et les royalties
---
### 2.4 Recommandations stratégiques
#### 1. Les 5 actions les plus impactantes pour la stabilité
1. **Connecter le stream server gRPC au backend Go** (8h) — Sans ça, la chaîne audio est cassée pour le transcoding et les callbacks. Le stream server fonctionne en isolation mais ne communique pas les résultats au backend.
2. **Aligner les routes API social/search/inventory/live** (16h) — Soit créer les endpoints manquants côté Go, soit retirer les pages fantômes du frontend. 4 modules entiers sont en mode "MSW-only".
3. **Connecter la job queue** (8h) — Intégrer `asynq` ou un système similaire pour le transcoding async, les emails, et les thumbnails. Le service est un shell vide.
4. **Finaliser la migration UUID** (6h) — 6 FIXMEs dans le backend risquent des bugs runtime sur les opérations d'upload, collaborateurs de playlist, et historique.
5. **Fixer les 12 tests unitaires cassés et stabiliser les E2E** (5h) — Le LikeButton a un label i18n incorrect, ToastContext a un import cassé, et Playwright a un conflit de port.
#### 2. Choix architecturaux problématiques à l'échelle
- **Stream server gRPC stub** : L'architecture prévoit gRPC pour la communication inter-services, mais les deux implémentations (chat HTTP, stream stub) ne l'utilisent pas vraiment. Cela crée une incohérence architecturale. **Risque** : si le trafic augmente, la communication HTTP entre services ne passera pas à l'échelle aussi bien que gRPC.
- **Double source de vérité pour les services API** : Le frontend a des services à deux endroits (`src/services/*.ts` et `src/features/*/services/*.ts`). Certains endpoints sont appelés depuis les deux. **Risque** : maintenance difficile, bugs de désynchro.
- **Hyperswitch comme payment router** : Choix ambitieux (open-source, multi-provider) mais complexe à opérer. Pour un PoC, Stripe direct serait plus simple. **Risque** : overhead opérationnel important.
- **42 migrations SQL sans outil de migration formel** : Les migrations sont des fichiers SQL bruts. Pas de `migrate` CLI ou de tracking automatique des versions appliquées. **Risque** : conflits et migrations manquées en production.
#### 3. Modules surdéveloppés par rapport à leur priorité
- **Infrastructure Kubernetes** (`k8s/`) : Déploiements, HPA/VPA, monitoring Prometheus/Grafana/Loki, CDN (CloudFront, Cloudflare), certificats Let's Encrypt, network policies, backup cronjobs — tout ça pour un PoC qui n'a pas encore de version stable. **Surdéveloppé** par rapport à l'état du code applicatif.
- **Sécurité avancée (Module 20)** : 67% complété alors que le social (13%), l'analytics (7%), et la recherche (17%) sont très en retard. Le rate limiting multi-couche et les security headers sont parfaits mais disproportionnés pour un PoC.
- **CI/CD** (9 workflows GitHub Actions) : Pipeline complet avec vulnerability scans, SBOM, image signing, smoke tests post-deploy — excellent mais prématuré avant la stabilité fonctionnelle.
#### 4. Modules sous-développés critiques pour le PoC
- **Social & Communauté (13%)** : Pour une plateforme collaborative musicale, le social est le coeur du produit. Les features de feed, posts, groupes n'existent qu'en mocks MSW sans backend.
- **Recherche & Découverte (17%)** : La recherche est basique (fulltext sur tracks/users). Aucun filtre par BPM/key/genre — fonctionnalités critiques pour des musiciens.
- **Analytics (7%)** : Les créateurs ont besoin de voir leurs stats d'écoute. Le dashboard renvoie des données vides sur plusieurs endpoints.
#### 5. Estimation réaliste pour v0.101 stable
| Phase | Contenu | Effort |
|-------|---------|--------|
| Stabilisation technique | gRPC, job queue, UUID migration, tests | 30h |
| Alignement API frontend↔backend | Routes social, search, inventory, live | 20h |
| Core features manquantes | Recherche avancée, analytics basiques, chat complet | 40h |
| Polish & testing | E2E stable, Storybook audit, bug fixes | 20h |
| **TOTAL** | | **110h (~3 semaines à temps plein)** |
---
## SCORE GLOBAL DE MATURITÉ
### 32 / 100
**Détail :**
| Critère | Score | Pondération | Note |
|---------|-------|-------------|------|
| Compilation & santé du code | 95/100 | 15% | Tout compile, peu de TODOs critiques |
| Architecture & structure | 80/100 | 15% | Bien organisé mais incohérences gRPC/HTTP |
| Features TIER 0 | 44/100 | 25% | 44% des features V1 implémentées |
| Tests & qualité | 70/100 | 10% | 99.6% unit pass, E2E instable |
| Intégration inter-services | 30/100 | 15% | gRPC stub, routes orphelines, MSW-only pages |
| Documentation & DevEx | 75/100 | 5% | Bien documenté, env templates complets |
| Sécurité | 85/100 | 10% | Excellente pour un PoC |
| Infrastructure & Ops | 60/100 | 5% | Surdimensionné mais fonctionnel |
**Score pondéré : 32/100**
---
**Synthèse en une phrase** : Veza possède une base technique solide et bien architecturée (compilation propre, 3300+ tests, sécurité exemplaire, infrastructure K8s complète), mais reste à mi-chemin de la stabilité fonctionnelle : le stream server ne communique pas vraiment avec le backend, 4 modules frontend n'existent qu'en mocks, et seulement 44% des features TIER 0 sont implémentées de bout en bout — il faut environ 3 semaines de travail focalisé pour atteindre une v0.101 stable.

2
Untitled Normal file
View file

@ -0,0 +1,2 @@
continues les étapes de remédiation pour atteindre la version stable et fonctionnelle :
@103_audit_global_features_states.md @103_RAPPORT_ETAT_FEATURES_2026_02_16.md

View file

@ -43,7 +43,7 @@ RUN npm run build && \
du -sh dist/
# Production stage - nginx alpine
FROM nginx:alpine
FROM nginx:1.27-alpine
# Install minimal dependencies for healthcheck
RUN apk add --no-cache wget && \

View file

@ -41,7 +41,14 @@ async function globalSetup(config: FullConfig) {
const page = await context.newPage();
try {
// Step 1: Verify API is available before attempting login
// Step 1: Navigate to frontend first (required for relative API URLs - fetch needs a base URL)
console.log('🔧 [GLOBAL SETUP] Navigating to frontend...');
await page.goto(TEST_CONFIG.FRONTEND_URL, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
// Step 2: Verify API is available (page has base URL for relative fetch)
console.log('🔧 [GLOBAL SETUP] Verifying API availability...');
console.log(`🔧 [GLOBAL SETUP] API URL: ${TEST_CONFIG.API_URL}`);
@ -55,7 +62,6 @@ async function globalSetup(config: FullConfig) {
const healthResponse = await fetch(healthUrl, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(10000), // 10s timeout
});
return { success: healthResponse.ok, status: healthResponse.status };
@ -71,13 +77,6 @@ async function globalSetup(config: FullConfig) {
console.log('✅ [GLOBAL SETUP] API is available');
}
// Navigate to frontend root (not /login to avoid routing issues)
console.log('🔧 [GLOBAL SETUP] Navigating to frontend...');
await page.goto(TEST_CONFIG.FRONTEND_URL, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
// Login via API directly in the browser context
console.log('🔧 [GLOBAL SETUP] Attempting API login via browser...');
const loginResult = await page.evaluate(async ({ apiUrl, email, password }) => {

View file

@ -44,6 +44,8 @@ test.describe('Authentication Flow', () => {
// Attendre que le formulaire soit prêt (premier test peut être plus lent)
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { });
await expect(page.locator('form')).toBeVisible({ timeout: 10000 });
await expect(page.locator('input[type="email"], input[name="email"]').first()).toBeVisible({ timeout: 5000 });
await page.waitForTimeout(500);
// Remplir le formulaire
@ -99,6 +101,7 @@ test.describe('Authentication Flow', () => {
test('should show error with invalid credentials', async ({ page }) => {
await page.goto(`${TEST_CONFIG.FRONTEND_URL}/login`);
await page.waitForLoadState('domcontentloaded');
await expect(page.locator('form')).toBeVisible({ timeout: 10000 });
// Remplir avec des credentials invalides
await fillField(page, 'input[type="email"], input[name="email"]', 'wrong@example.com');
@ -139,7 +142,9 @@ test.describe('Authentication Flow', () => {
const twoFaInput = page.locator('input#2fa-code, input[placeholder="000000"]').first();
await expect(twoFaInput).toBeVisible({ timeout: 10000 });
await twoFaInput.fill(code);
await page.locator('button:has-text("Verify")').first().click();
const verifyButton = page.locator('button:has-text("Verify")').first();
await expect(verifyButton).toBeVisible({ timeout: 5000 });
await verifyButton.click();
await expect(page).toHaveURL(/\/(dashboard|$)/, { timeout: 15000 });
const token = await getAuthToken(page);
@ -155,6 +160,8 @@ test.describe('Authentication Flow', () => {
// Attendre que la page soit complètement chargée
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await expect(page.locator('form')).toBeVisible({ timeout: 10000 });
await expect(page.locator('input[name="email"], input#email').first()).toBeVisible({ timeout: 5000 });
// Générer un email unique pour éviter les conflits
const uniqueEmail = `test-${Date.now()}@example.com`;
@ -251,6 +258,7 @@ test.describe('Authentication Flow', () => {
// Attendre que la page soit complètement chargée
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await expect(page.locator('form')).toBeVisible({ timeout: 10000 });
// Utiliser un email qui existe déjà (celui du test user)
const password = 'Str0ng!P@ssw0rd2024'; // 12+ caractères requis, fort
@ -329,6 +337,7 @@ test.describe('Authentication Flow', () => {
const isUserMenuVisible = await userMenu.isVisible().catch(() => false);
if (isUserMenuVisible) {
await expect(userMenu).toBeVisible({ timeout: 5000 });
await userMenu.click();
await page.waitForTimeout(500); // Attendre que le menu s'ouvre
@ -408,8 +417,12 @@ test.describe('Authentication Flow', () => {
// Refresh page
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000); // Wait for app to check auth status
// Verify nav/sidebar visible (confirms authenticated UI)
await expect(page.locator('nav[role="navigation"], aside[role="navigation"]')).toBeVisible({ timeout: 10000 });
// Check if still authenticated
const afterRefresh = await page.evaluate(() => {
try {
@ -450,6 +463,7 @@ test.describe('Authentication Flow', () => {
// Try submitting the form with invalid data
const submitButton = page.locator('button[type="submit"]').first();
await expect(submitButton).toBeVisible({ timeout: 5000 });
await submitButton.click();
await page.waitForTimeout(2000); // Wait to see if navigation happens
@ -486,7 +500,8 @@ test.describe('Authentication Flow', () => {
await page.waitForLoadState('domcontentloaded');
// Attendre que la page soit complètement chargée
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => { });
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await expect(page.locator('form')).toBeVisible({ timeout: 10000 });
// Remplir avec des mots de passe différents
await fillField(page, 'input[name="email"], input#email', 'newuser@example.com');

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View file

@ -222,9 +222,12 @@ test.describe('Playlists CRUD', () => {
// Utiliser dispatchEvent pour contourner l'overlay de la sidebar qui intercepte le click
await editButton.dispatchEvent('click');
} else if (isMoreVisible) {
await expect(moreButton).toBeVisible({ timeout: 5000 });
await moreButton.click();
await page.waitForTimeout(500);
await page.locator('[role="menuitem"]:has-text("Edit"), [role="menuitem"]:has-text("Éditer")').first().click();
const editMenuItem = page.locator('[role="menuitem"]:has-text("Edit"), [role="menuitem"]:has-text("Éditer")').first();
await expect(editMenuItem).toBeVisible({ timeout: 5000 });
await editMenuItem.click();
} else {
// Si pas de bouton d'édition visible, on est peut-être déjà sur la page de détails
// Chercher un formulaire d'édition ou un bouton pour ouvrir l'édition
@ -244,6 +247,7 @@ test.describe('Playlists CRUD', () => {
// Soumettre en cliquant sur "Enregistrer" (pas de balise form dans le dialog)
// await forceSubmitForm(page, 'form'); // Ne marche pas car pas de form
const saveButton = page.locator('[role="dialog"] button').filter({ hasText: /enregistrer/i }).first();
await expect(saveButton).toBeVisible({ timeout: 5000 });
await saveButton.click({ force: true });
await waitForToast(page, 'success', 10000);
@ -315,11 +319,15 @@ test.describe('Playlists CRUD', () => {
const isMoreVisible = await moreButton.isVisible().catch(() => false);
if (isAddVisible) {
await expect(addToPlaylistButton).toBeVisible({ timeout: 5000 });
await addToPlaylistButton.click();
} else if (isMoreVisible) {
await expect(moreButton).toBeVisible({ timeout: 5000 });
await moreButton.click();
await page.waitForTimeout(500);
await page.locator('[role="menuitem"]:has-text("Add to playlist"), [role="menuitem"]:has-text("Ajouter")').first().click();
const addMenuItem = page.locator('[role="menuitem"]:has-text("Add to playlist"), [role="menuitem"]:has-text("Ajouter")').first();
await expect(addMenuItem).toBeVisible({ timeout: 5000 });
await addMenuItem.click();
} else {
console.warn('⚠️ [PLAYLISTS] Add to playlist button not found, skipping test');
test.skip();
@ -333,6 +341,7 @@ test.describe('Playlists CRUD', () => {
const isPlaylistOptionVisible = await playlistOption.isVisible({ timeout: 5000 }).catch(() => false);
if (isPlaylistOptionVisible) {
await expect(playlistOption).toBeVisible({ timeout: 5000 });
await playlistOption.click();
await waitForToast(page, 'success', 10000);
console.log('✅ [PLAYLISTS] Track added to playlist successfully');
@ -386,11 +395,15 @@ test.describe('Playlists CRUD', () => {
const isMoreVisible = await moreButton.isVisible().catch(() => false);
if (isDeleteVisible) {
await expect(deleteButton).toBeVisible({ timeout: 5000 });
await deleteButton.click({ force: true });
} else if (isMoreVisible) {
await expect(moreButton).toBeVisible({ timeout: 5000 });
await moreButton.click();
await page.waitForTimeout(500);
await page.locator('[role="menuitem"]:has-text("Delete"), [role="menuitem"]:has-text("Supprimer")').first().click();
const deleteMenuItem = page.locator('[role="menuitem"]:has-text("Delete"), [role="menuitem"]:has-text("Supprimer")').first();
await expect(deleteMenuItem).toBeVisible({ timeout: 5000 });
await deleteMenuItem.click();
} else {
// Fallback: icône de corbeille
const trashButton = page.locator('button svg.lucide-trash, button svg.fa-trash').first();
@ -408,6 +421,7 @@ test.describe('Playlists CRUD', () => {
const isConfirmVisible = await confirmButton.isVisible().catch(() => false);
if (isConfirmVisible) {
await expect(confirmButton).toBeVisible({ timeout: 5000 });
await confirmButton.click({ force: true });
// 🔴 FIX: Attendre la confirmation de suppression avant de continuer
// Sinon la navigation manuelle suivante peut annuler la requête

View file

@ -56,6 +56,7 @@ test.describe('User Profile Management', () => {
const isSidebarLinkVisible = await profileLinkSidebar.isVisible({ timeout: 3000 }).catch(() => false);
if (isSidebarLinkVisible) {
await expect(profileLinkSidebar).toBeVisible({ timeout: 5000 });
await profileLinkSidebar.click();
} else {
// Méthode 2: Via menu utilisateur (Avatar dropdown)
@ -63,6 +64,7 @@ test.describe('User Profile Management', () => {
const isUserMenuVisible = await userMenu.isVisible().catch(() => false);
if (isUserMenuVisible) {
await expect(userMenu).toBeVisible({ timeout: 5000 });
await userMenu.click();
await page.waitForTimeout(500);
await page.locator('[role="menuitem"]:has-text("Profil"), [role="menuitem"]:has-text("Profile")').first().click();
@ -143,6 +145,7 @@ test.describe('User Profile Management', () => {
if (isDisabled) {
const editButton = page.locator('button:has-text("Edit"), button:has-text("Modifier"), button:has-text("profile.edit")').first();
if (await editButton.isVisible({ timeout: 5000 }).catch(() => false)) {
await expect(editButton).toBeVisible({ timeout: 5000 });
await editButton.click();
await page.waitForTimeout(500); // Attendre que le mode édition s'active
// Re-vérifier que le champ est maintenant éditable
@ -364,6 +367,7 @@ test.describe('User Profile Management', () => {
const isAvatarContainerVisible = await avatarContainer.isVisible().catch(() => false);
if (isAvatarContainerVisible) {
await expect(avatarContainer).toBeVisible({ timeout: 5000 });
await avatarContainer.click();
await page.waitForTimeout(500);
} else {

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 KiB

View file

@ -1,16 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e2]:
- link "Skip to content" [ref=e3] [cursor=pointer]:
- /url: "#main-content"
- generic [ref=e5]:
- img [ref=e7]
- heading "Failed to load Register" [level=2] [ref=e9]
- paragraph [ref=e10]: Cannot convert object to primitive value
- generic [ref=e11]:
- button "Try Again" [ref=e12]:
- img [ref=e13]
- text: Try Again
- button "Refresh Page" [ref=e18]
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 KiB

View file

@ -1,51 +0,0 @@
# Page snapshot
```yaml
- generic [ref=e2]:
- link "Skip to content" [ref=e3] [cursor=pointer]:
- /url: "#main-content"
- main "Page d'authentification" [ref=e4]:
- generic [ref=e10]:
- generic [ref=e11]:
- generic [ref=e12]:
- generic [ref=e14]: V
- generic [ref=e15]: Veza
- heading "Welcome Back" [level=1] [ref=e16]
- paragraph [ref=e17]: Sign in to your account
- generic "Welcome Back" [ref=e18]:
- generic [ref=e20]:
- generic [ref=e21]:
- button "Se connecter avec Google" [ref=e22]:
- img [ref=e23]
- generic [ref=e28]: Google
- button "Se connecter avec GitHub" [ref=e29]:
- img [ref=e30]
- generic [ref=e33]: GitHub
- button "Se connecter avec Discord" [ref=e34]:
- img [ref=e35]
- generic [ref=e37]: Discord
- generic [ref=e42]: or continue with
- generic [ref=e43]:
- generic [ref=e44]:
- generic [ref=e45]:
- generic [ref=e46]: Email
- textbox "Email" [ref=e48]
- generic [ref=e49]:
- generic [ref=e50]: Password
- generic [ref=e51]:
- textbox "Password" [ref=e52]
- button "Show password" [ref=e53]:
- img [ref=e54]
- generic [ref=e57]:
- generic [ref=e58] [cursor=pointer]:
- generic [ref=e59]:
- checkbox "Remember me" [ref=e60]
- img
- generic [ref=e62]: Remember me
- link "Forgot password?" [ref=e63] [cursor=pointer]:
- /url: /forgot-password
- button "Sign In" [ref=e64]
- navigation "Navigation d'authentification" [ref=e65]:
- link "Don't have an account? Sign up" [ref=e66] [cursor=pointer]:
- /url: /register
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 734 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 386 KiB

File diff suppressed because one or more lines are too long

View file

@ -8,10 +8,22 @@ const NAVIGATION_TIMEOUT_MS = 30000; // 30s per story navigation
const DEFAULT_TIMEOUT_MS = 300000; // 300s global default for Playwright
const MAX_RETRIES = 2; // 2 retries = 3 attempts total
const RETRY_DELAY_MS = 1500;
const POST_LOAD_WAIT_MS = 150;
const POST_LOAD_WAIT_MS = 600; // Allow MSW and async requests to settle
console.log("Script started");
/** Console errors that are intentional or from Storybook internals (non-blocking). */
const IGNORED_CONSOLE_ERRORS = [
/This is a test error for demonstrating ErrorBoundary/i,
/received.*but was unable to determine the source of the event/i,
];
function isIgnoredConsoleError(text, locationUrl = '') {
if (IGNORED_CONSOLE_ERRORS.some((re) => re.test(text))) return true;
if (/sb-manager\/|sb-addons\//.test(locationUrl || '')) return true;
return false;
}
/** Ignore Storybook manager/addon requests; only count app and API failures. */
function isAppRelevantFailure(url) {
try {
@ -40,6 +52,7 @@ async function processStory(page, storyId, report) {
storyDetails.console.push({
type: msg.type(),
text: msg.text(),
url: location.url,
location: `${location.url}:${location.lineNumber}:${location.columnNumber}`
});
}
@ -53,6 +66,8 @@ async function processStory(page, storyId, report) {
const failure = request.failure();
const errorText = failure ? failure.errorText : 'Unknown network error';
if (errorText === 'net::ERR_ABORTED' && /\/iframe\.html$|\/iframe$/.test(new URL(url).pathname || '')) return;
// ERR_ABORTED often occurs when navigating away before requests complete; not a blocking error
if (errorText === 'net::ERR_ABORTED') return;
storyDetails.network.push({ url, method: request.method(), errorText });
};
@ -81,8 +96,11 @@ async function processStory(page, storyId, report) {
page.removeAllListeners('pageerror');
page.removeAllListeners('requestfailed');
const errorCount = storyDetails.console.filter(c => c.type === 'error').length +
storyDetails.pageErrors.length +
const blockingConsoleErrors = storyDetails.console.filter(
(c) => c.type === 'error' && !isIgnoredConsoleError(c.text, c.url)
);
const errorCount = blockingConsoleErrors.length +
storyDetails.pageErrors.filter((e) => !isIgnoredConsoleError(e.message)).length +
storyDetails.network.length +
(storyDetails.navigation ? 1 : 0);
@ -118,7 +136,10 @@ async function audit() {
throw new Error(`File not found: ${storybookStaticPath}`);
}
const indexJson = JSON.parse(fs.readFileSync(storybookStaticPath, 'utf8'));
stories = Object.values(indexJson.entries).map(e => e.id);
let allStories = Object.values(indexJson.entries).map(e => e.id);
const limit = parseInt(process.env.STORYBOOK_AUDIT_LIMIT || '0', 10);
stories = limit > 0 ? allStories.slice(0, limit) : allStories;
if (limit > 0) console.log(`Limiting audit to first ${limit} stories`);
} catch (e) {
console.error(`Failed to read local index.json: ${e.message}`);
process.exit(1);

View file

@ -0,0 +1,88 @@
/**
* Tests for AdminDashboardView
* Plan 2.4: Loading, Error, Success states
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ToastProvider } from '@/components/feedback/ToastProvider';
import { AdminDashboardView } from '../AdminDashboardView';
import { useAdminDashboardView } from './useAdminDashboardView';
vi.mock('./useAdminDashboardView');
const defaultHookReturn = {
stats: {
totalUsers: 100,
monthlyRevenue: 5000,
activeSessions: 25,
pendingReports: 3,
trends: { users: '+5%', revenue: '+10%', sessions: '-2%', reports: '+1' },
},
reports: [],
uploads: [],
auditLogs: [],
loading: false,
error: null,
protocolActive: null,
handleAction: vi.fn(),
triggerProtocol: vi.fn(),
retry: vi.fn(),
};
function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<ToastProvider>
<MemoryRouter>{children}</MemoryRouter>
</ToastProvider>
</QueryClientProvider>
);
}
describe('AdminDashboardView', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useAdminDashboardView).mockReturnValue(defaultHookReturn as any);
});
it('should render success state with stats', () => {
render(<AdminDashboardView />, { wrapper: createWrapper() });
expect(screen.getByText(/Total Nodes/i)).toBeInTheDocument();
expect(screen.getByText(/100/)).toBeInTheDocument();
});
it('should render loading state', () => {
vi.mocked(useAdminDashboardView).mockReturnValue({
...defaultHookReturn,
loading: true,
} as any);
render(<AdminDashboardView />, { wrapper: createWrapper() });
expect(screen.queryByText(/Total Nodes/i)).not.toBeInTheDocument();
});
it('should render error state with retry', () => {
vi.mocked(useAdminDashboardView).mockReturnValue({
...defaultHookReturn,
loading: false,
error: new Error('Failed to load admin dashboard'),
} as any);
render(<AdminDashboardView />, { wrapper: createWrapper() });
expect(screen.getByText(/Failed to load admin dashboard/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /retry|réessayer/i })).toBeInTheDocument();
});
});

View file

@ -31,7 +31,7 @@ export function useCreateProductView() {
const handlePublish = useCallback(async () => {
if (!title || !description) {
addToast('Please fill in required fields', 'error');
toast.error('Please fill in required fields');
return;
}
const activeLicense = licenses.find((l) => l.enabled);

View file

@ -24,7 +24,7 @@ export function TwoFactorSetup({ onBack, onComplete }: TwoFactorSetupProps) {
const handleCopySecret = () => {
if (setupData) {
navigator.clipboard.writeText(setupData.secret);
addToast('Secret Key copied');
toast.success('Secret Key copied');
}
};

View file

@ -4,7 +4,6 @@ import { twoFactorService } from '@/services/2fa-service';
import type { TwoFactorSetupData, TwoFactorMethod, TwoFactorSetupStep } from './types';
export function useTwoFactorSetup(onBack: () => void, _onComplete: () => void) {
const { addToast } = useToast();
const [step, setStep] = useState<TwoFactorSetupStep>(1);
const [method, setMethod] = useState<TwoFactorMethod>('totp');
const [verificationCode, setVerificationCode] = useState('');

View file

@ -28,7 +28,6 @@ interface ExploreItem {
}
export const ExploreView: React.FC = () => {
const { addToast } = useToast();
const [activeTab, setActiveTab] = useState<
'for_you' | 'trending' | 'new' | 'popular'
>('for_you');

View file

@ -37,8 +37,8 @@ export function useGroupDetailView(groupId: string) {
if (!group) return;
await groupService.join(group.id);
setGroup({ ...group, userRole: 'member', members: group.members + 1 });
addToast('Joined group!', 'success');
}, [group, addToast]);
toast.success('Joined group!');
}, [group]);
const handleLeave = useCallback(async () => {
if (!group) return;

View file

@ -47,11 +47,11 @@ export const FEATURES = {
/**
* HLS Streaming
* Backend endpoints: /api/v1/tracks/:id/hls/info, /api/v1/tracks/:id/hls/status (NOT IMPLEMENTED)
* Backend endpoints: /api/v1/tracks/:id/hls/info, /api/v1/tracks/:id/hls/status
*/
HLS_STREAMING: parseFeatureEnv(
import.meta.env.VITE_FEATURE_HLS_STREAMING,
false,
true,
),
/**

View file

@ -3,6 +3,28 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ToastProvider, useToast } from '../components/feedback/ToastProvider';
import { ReactNode } from 'react';
// Mock toast to avoid react-hot-toast dynamic import issues in Vitest
vi.mock('@/utils/toast', () => {
const mockFn = (msg: string) => {
const el = document.createElement('div');
el.textContent = msg;
el.setAttribute('data-testid', 'toast-message');
document.body.appendChild(el);
};
return {
default: Object.assign(mockFn, {
success: mockFn,
error: mockFn,
warning: mockFn,
loading: mockFn,
custom: mockFn,
dismiss: vi.fn(),
remove: vi.fn(),
promise: vi.fn(() => Promise.resolve()),
}),
};
});
const wrapper = ({ children }: { children: ReactNode }) => (
<ToastProvider>{children}</ToastProvider>
);
@ -10,6 +32,7 @@ const wrapper = ({ children }: { children: ReactNode }) => (
describe('ToastContext', () => {
beforeEach(() => {
vi.clearAllMocks();
document.querySelectorAll('[data-testid="toast-message"]').forEach((el) => el.remove());
});
it('should provide toast context', () => {

View file

@ -21,4 +21,4 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Default: Story = { args: undefined as never };

View file

@ -2,7 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AnimatedNumber } from '@/components/ui/AnimatedNumber';
import { cn } from '@/lib/utils';
import { useTranslation } from '@/hooks/useTranslation';
import { LucideIcon, Music, MessageSquare, Heart, Users } from 'lucide-react';
import { Music, MessageSquare, Heart, Users } from 'lucide-react';
const STATS = [
{

View file

@ -0,0 +1,87 @@
/**
* Cart component tests - Marketplace feature
* Plan V0.101: Tests marketplace 10
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@/test/test-utils';
import userEvent from '@testing-library/user-event';
import { Cart } from './Cart';
import { useCartStore } from '@/stores/cartStore';
import type { Product } from '@/types/marketplace';
vi.mock('@/services/marketplaceService', () => ({
marketplaceService: {
createOrder: vi.fn(),
},
}));
vi.mock('@/features/auth/store/authStore', () => ({
useAuthStore: () => ({ isAuthenticated: true }),
}));
vi.mock('@/hooks/useToast', () => ({
useToast: () => ({
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() },
addToast: vi.fn(),
}),
}));
const mockProduct: Product = {
id: 'prod-1',
seller_id: 'seller-1',
title: 'Test Track',
description: 'A test product',
price: 19.99,
currency: 'EUR',
status: 'active',
product_type: 'track',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-01T00:00:00Z',
};
describe('Cart', () => {
beforeEach(() => {
useCartStore.getState().clearCart();
vi.clearAllMocks();
});
it('should display empty state when cart is empty', () => {
render(<Cart isOpen={true} onClose={() => {}} />);
expect(screen.getByText(/your cart is empty/i)).toBeInTheDocument();
});
it('should display cart items when cart has products', () => {
useCartStore.getState().addItem(mockProduct);
render(<Cart isOpen={true} onClose={() => {}} />);
expect(screen.getByText(mockProduct.title)).toBeInTheDocument();
expect(screen.getAllByText(/19,99/).length).toBeGreaterThan(0);
});
it('should display total for cart items', () => {
useCartStore.getState().addItem(mockProduct);
useCartStore.getState().addItem({
...mockProduct,
id: 'prod-2',
title: 'Second',
price: 10,
});
render(<Cart isOpen={true} onClose={() => {}} />);
expect(screen.getByText('29,99 €')).toBeInTheDocument();
});
it('should call createOrder on checkout when authenticated', async () => {
const { marketplaceService } = await import('@/services/marketplaceService');
useCartStore.getState().addItem(mockProduct);
(marketplaceService.createOrder as ReturnType<typeof vi.fn>).mockResolvedValue({ order: { id: 'ord-1' } });
const user = userEvent.setup();
render(<Cart isOpen={true} onClose={() => {}} />);
const checkoutBtn = screen.getByRole('button', { name: /checkout/i });
await user.click(checkoutBtn);
await waitFor(() => {
expect(marketplaceService.createOrder).toHaveBeenCalledWith([{ product_id: 'prod-1' }]);
});
});
});

View file

@ -0,0 +1,63 @@
/**
* NotificationsPage tests - Plan V0.101: Tests Notifications 3
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@/test/test-utils';
import userEvent from '@testing-library/user-event';
import { NotificationsPage } from './NotificationsPage';
import { NotificationsPageEmpty } from './NotificationsPageEmpty';
import { NotificationsPageItem } from './NotificationsPageItem';
import type { Notification } from '../../services/notificationService';
const mockNotification: Notification = {
id: 'n1',
user_id: 'u1',
type: 'track_uploaded',
title: 'New track',
content: 'A new track was uploaded',
read: false,
created_at: new Date().toISOString(),
};
describe('NotificationsPage', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should display empty state when no notifications', () => {
render(<NotificationsPageEmpty filterType="all" />);
expect(screen.getByRole('heading', { name: /no notifications/i })).toBeInTheDocument();
});
it('should display notification item with title and content', () => {
const onMarkAsRead = vi.fn();
render(
<NotificationsPageItem
notification={mockNotification}
onMarkAsRead={onMarkAsRead}
/>
);
expect(screen.getByText('New track')).toBeInTheDocument();
expect(screen.getByText(/a new track was uploaded/i)).toBeInTheDocument();
});
it('should call onMarkAsRead when mark-as-read button is clicked', async () => {
const onMarkAsRead = vi.fn();
const user = userEvent.setup();
render(
<NotificationsPageItem
notification={mockNotification}
onMarkAsRead={onMarkAsRead}
/>
);
const markAsReadBtn = screen.getByRole('button', { name: /mark as read/i });
await user.click(markAsReadBtn);
expect(onMarkAsRead).toHaveBeenCalledWith('n1');
});
});

View file

@ -251,15 +251,23 @@ export const usePlayerStore = create<PlayerStore>()(
currentTrack: state.currentTrack,
}),
migrate: (persistedState: unknown) => {
const s = persistedState as Partial<PlayerStore> | null;
const s = persistedState as Record<string, unknown> | null;
const rawRepeat = s?.repeat as string | undefined;
// Map legacy 'one'/'all' to 'track'/'playlist' for backward compatibility
const repeat: 'off' | 'track' | 'playlist' =
rawRepeat === 'one'
? 'track'
: rawRepeat === 'all'
? 'playlist'
: (rawRepeat === 'track' || rawRepeat === 'playlist' ? rawRepeat : 'off');
return {
volume: s?.volume ?? 100,
muted: s?.muted ?? false,
repeat: (s?.repeat as 'off' | 'one' | 'all') ?? 'off',
shuffle: s?.shuffle ?? false,
queue: s?.queue ?? [],
currentIndex: s?.currentIndex ?? -1,
currentTrack: s?.currentTrack ?? null,
volume: (s?.volume as number | undefined) ?? 100,
muted: (s?.muted as boolean | undefined) ?? false,
repeat,
shuffle: (s?.shuffle as boolean | undefined) ?? false,
queue: (s?.queue as Track[]) ?? [],
currentIndex: (s?.currentIndex as number | undefined) ?? -1,
currentTrack: (s?.currentTrack as Track | null) ?? null,
};
},
},

View file

@ -277,9 +277,7 @@ export interface GetRecommendationsParams {
/**
* Rechercher des playlists
*
* MVP: This feature is disabled. Backend endpoint is not implemented.
* TODO: Enable when backend implements GET /api/v1/playlists/search
* Backend: GET /api/v1/playlists/search
*
* @see FEATURES.PLAYLIST_SEARCH
*/
@ -296,9 +294,7 @@ export async function searchPlaylists(
/**
* Créer un lien de partage
*
* MVP: This feature is disabled. Backend endpoint is not implemented.
* TODO: Enable when backend implements POST /api/v1/playlists/:id/share
* Backend: POST /api/v1/playlists/:id/share
*
* @see FEATURES.PLAYLIST_SHARE
*/
@ -336,23 +332,31 @@ export async function removeTrackFromPlaylist(
/**
* Obtenir des recommandations de playlists
*
* MVP: This feature is disabled. Backend endpoint is not implemented.
* TODO: Enable when backend implements GET /api/v1/playlists/recommendations
* Backend: GET /api/v1/playlists/recommendations
*
* @see FEATURES.PLAYLIST_RECOMMENDATIONS
*/
export async function getPlaylistRecommendations(
_params: GetRecommendationsParams,
params: GetRecommendationsParams,
): Promise<{ recommendations: PlaylistRecommendation[] }> {
requireFeature('PLAYLIST_RECOMMENDATIONS');
// TODO: Replace with actual API call when backend is ready
// const response = await apiClient.get<{ recommendations: PlaylistRecommendation[] }>('/playlists/recommendations', { params });
// return response.data;
// Mock response for now to satisfy type checker and frontend dev
return Promise.resolve({
recommendations: [],
return wrapPlaylistError(async () => {
const response = await apiClient.get<{
data?: { recommendations: PlaylistRecommendation[] };
recommendations?: PlaylistRecommendation[];
}>('/playlists/recommendations', {
params: {
limit: params.limit,
min_score: params.min_score,
include_own: params.include_own,
},
});
const data = response.data as Record<string, unknown> | undefined;
const recommendations =
(data?.data as { recommendations?: PlaylistRecommendation[] })?.recommendations ??
(data?.recommendations as PlaylistRecommendation[] | undefined) ??
[];
return { recommendations };
});
}

View file

@ -0,0 +1,195 @@
/**
* AssignRoleModal tests - RBAC feature
* Plan V0.101: Tests RBAC 5
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor, within } from '@/test/test-utils';
import userEvent from '@testing-library/user-event';
import { AssignRoleModal } from './AssignRoleModal';
import * as roleService from '../services/roleService';
import type { Role } from '../types/role';
vi.mock('../services/roleService');
const mockToastError = vi.fn();
vi.mock('@/hooks/useToast', () => ({
useToast: () => ({
success: vi.fn(),
error: mockToastError,
}),
}));
const mockRoles: Role[] = [
{
id: 'r1',
name: 'admin',
display_name: 'Administrator',
description: 'Full access',
is_system: false,
is_active: true,
created_at: '',
updated_at: '',
permissions: [],
},
{
id: 'r2',
name: 'editor',
display_name: 'Editor',
description: 'Can edit content',
is_system: false,
is_active: true,
created_at: '',
updated_at: '',
permissions: [],
},
];
describe('AssignRoleModal', () => {
const onClose = vi.fn();
const onRoleAssigned = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
mockToastError.mockClear();
vi.mocked(roleService.getUserRoles).mockResolvedValue([]);
vi.mocked(roleService.assignRole).mockResolvedValue(undefined);
});
it('should display modal with title when open', async () => {
render(
<AssignRoleModal
open={true}
userId="u1"
userName="John Doe"
availableRoles={mockRoles}
onClose={onClose}
onRoleAssigned={onRoleAssigned}
/>
);
await waitFor(() => {
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText(/assign role to john doe/i)).toBeInTheDocument();
});
});
it('should load user roles on open and display available roles', async () => {
const user = userEvent.setup();
vi.mocked(roleService.getUserRoles).mockResolvedValue([]);
render(
<AssignRoleModal
open={true}
userId="u1"
availableRoles={mockRoles}
onClose={onClose}
onRoleAssigned={onRoleAssigned}
/>
);
await waitFor(() => {
expect(roleService.getUserRoles).toHaveBeenCalledWith('u1');
});
// Open the Select dropdown to see role options
const dialog = screen.getByRole('dialog');
const selectTriggers = within(dialog).getAllByRole('button', { name: /select a role/i });
await user.click(selectTriggers[0]!);
await waitFor(() => {
expect(screen.getByRole('option', { name: /administrator/i })).toBeInTheDocument();
expect(screen.getByRole('option', { name: /editor/i })).toBeInTheDocument();
});
});
it('should call assignRole when submitting with selected role', async () => {
const user = userEvent.setup();
render(
<AssignRoleModal
open={true}
userId="u1"
availableRoles={mockRoles}
onClose={onClose}
onRoleAssigned={onRoleAssigned}
/>
);
await waitFor(() => {
expect(screen.getByText(/select a role/i)).toBeInTheDocument();
});
const dialog = screen.getByRole('dialog');
const selectTriggers = within(dialog).getAllByRole('button', { name: /select a role/i });
await user.click(selectTriggers[0]!);
const option = await screen.findByRole('option', { name: /administrator/i });
await user.click(option);
const assignBtn = within(dialog).getByRole('button', { name: /^assign role$/i });
await user.click(assignBtn);
await waitFor(() => {
expect(roleService.assignRole).toHaveBeenCalledWith('u1', {
role_id: 'r1',
expires_at: undefined,
});
});
});
it('should display current user roles when user has roles', async () => {
vi.mocked(roleService.getUserRoles).mockResolvedValue([mockRoles[0]]);
render(
<AssignRoleModal
open={true}
userId="u1"
availableRoles={mockRoles}
onClose={onClose}
onRoleAssigned={onRoleAssigned}
/>
);
await waitFor(() => {
expect(screen.getByText(/current roles/i)).toBeInTheDocument();
expect(screen.getByText(/administrator/i)).toBeInTheDocument();
});
});
it('should handle API 403 error when assigning role', async () => {
vi.mocked(roleService.assignRole).mockRejectedValue(
new Error('Forbidden: You do not have permission to assign roles')
);
const user = userEvent.setup();
render(
<AssignRoleModal
open={true}
userId="u1"
availableRoles={mockRoles}
onClose={onClose}
onRoleAssigned={onRoleAssigned}
/>
);
await waitFor(() => {
expect(screen.getByText(/select a role/i)).toBeInTheDocument();
});
const dialog = screen.getByRole('dialog');
const selectTriggers = within(dialog).getAllByRole('button', { name: /select a role/i });
await user.click(selectTriggers[0]!);
const option = await screen.findByRole('option', { name: /administrator/i });
await user.click(option);
const assignBtn = within(dialog).getByRole('button', { name: /^assign role$/i });
await user.click(assignBtn);
await waitFor(() => {
expect(roleService.assignRole).toHaveBeenCalled();
expect(mockToastError).toHaveBeenCalledWith(
expect.stringMatching(/forbidden|permission/i)
);
});
});
});

View file

@ -75,8 +75,6 @@ export function AssignRoleModal({
setSelectedRoleId('');
setExpiresAt('');
onRoleAssigned();
setExpiresAt('');
onRoleAssigned();
} catch (err: unknown) {
const apiError = parseApiError(err);
error(apiError.message);

View file

@ -0,0 +1,77 @@
/**
* SearchPage tests - Plan V0.101: Tests Search 3
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@/test/test-utils';
import { SearchPage } from './SearchPage';
import { useSearchPage } from './useSearchPage';
import type { SearchResults } from '@/types/search';
vi.mock('./useSearchPage');
const mockSearchResults: SearchResults = {
tracks: [
{
id: 't1',
title: 'Test Track',
artist: 'Test Artist',
created_at: '2024-01-01T00:00:00Z',
} as SearchResults['tracks'][0],
],
artists: [
{
id: 'u1',
username: 'testuser',
avatar_url: undefined,
followers_count: 10,
},
],
playlists: [],
};
describe('SearchPage', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useSearchPage).mockReturnValue({
query: 'test',
setQuery: vi.fn(),
results: mockSearchResults,
isLoading: false,
error: null,
clearSearch: vi.fn(),
hasResults: true,
});
});
it('should display search results with tracks and artists', () => {
render(<SearchPage />);
expect(screen.getByText(/all results/i)).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /tracks \(1\)/i })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: /artists \(1\)/i })).toBeInTheDocument();
});
it('should display empty state when no results', () => {
vi.mocked(useSearchPage).mockReturnValue({
query: 'nonexistent',
setQuery: vi.fn(),
results: { tracks: [], artists: [], playlists: [] },
isLoading: false,
error: null,
clearSearch: vi.fn(),
hasResults: false,
});
render(<SearchPage />);
expect(screen.getByText(/no results found/i)).toBeInTheDocument();
});
it('should display search input with query value', () => {
render(<SearchPage />);
const input = screen.getByPlaceholderText(/search for tracks/i);
expect(input).toHaveValue('test');
});
});

View file

@ -8,6 +8,7 @@ import {
unlikeTrack,
getTrackLikes,
} from '../services/interactionService';
import { TrackServiceError } from '../errors/trackErrors';
import { useToast } from '@/hooks/useToast';
// Mock dependencies
@ -103,7 +104,7 @@ describe('LikeButton', () => {
await waitFor(() => {
const button = screen.getByRole('button');
expect(button).toHaveClass('text-red-500');
expect(button).toHaveClass('text-destructive');
});
});
@ -244,7 +245,7 @@ describe('LikeButton', () => {
it('should show error toast on like failure', async () => {
const user = userEvent.setup();
const error = new TrackUploadError('Failed to like track', 'SERVER', true);
const error = new TrackServiceError('Failed to like track', 'SERVER', true);
vi.mocked(getTrackLikes).mockResolvedValue({
count: 5,
isLiked: false,
@ -267,7 +268,7 @@ describe('LikeButton', () => {
it('should show error toast on unlike failure', async () => {
const user = userEvent.setup();
const error = new TrackUploadError(
const error = new TrackServiceError(
'Failed to unlike track',
'SERVER',
true,
@ -294,7 +295,7 @@ describe('LikeButton', () => {
it('should revert state on error', async () => {
const user = userEvent.setup();
const error = new TrackUploadError('Failed to like track', 'SERVER', true);
const error = new TrackServiceError('Failed to like track', 'SERVER', true);
vi.mocked(getTrackLikes).mockResolvedValue({
count: 5,
isLiked: false,
@ -313,12 +314,12 @@ describe('LikeButton', () => {
// Should revert to original state
await waitFor(() => {
expect(screen.getByText('5')).toBeInTheDocument();
expect(button).not.toHaveClass('text-red-500');
expect(button).not.toHaveClass('text-destructive');
});
});
it('should reload likes when trackId changes', async () => {
const { rerender } = render(<LikeButton trackId="1" />);
const { rerender } = render(<LikeButton trackId="1" />, { wrapper: createWrapper() });
vi.mocked(getTrackLikes).mockResolvedValue({
count: 5,
@ -348,7 +349,7 @@ describe('LikeButton', () => {
isLiked: false,
});
render(<LikeButton trackId="1" className="custom-class" />);
render(<LikeButton trackId="1" className="custom-class" />, { wrapper: createWrapper() });
await waitFor(() => {
const button = screen.getByRole('button');
@ -366,7 +367,7 @@ describe('LikeButton', () => {
await waitFor(() => {
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-label', 'Ajouter un like');
expect(button).toHaveAttribute('aria-label', 'Ajouter aux favoris');
});
});
@ -380,7 +381,7 @@ describe('LikeButton', () => {
await waitFor(() => {
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-label', 'Retirer le like');
expect(button).toHaveAttribute('aria-label', 'Retirer des favoris');
});
});
});

View file

@ -1,114 +0,0 @@
/**
* Webhook API - Feature API Layer
* Action 6.1.1.10: Update feature API files to use services
*
* This file provides the implementation layer for webhook API operations.
* Currently, there is no unified service layer for webhooks.
*
* TODO: Consider creating @/services/api/webhooks (webhooksApi) in the future
* to align with the service layer pattern used for tracks, auth, users, and playlists.
*/
import { apiClient } from '@/services/api/client';
import { Webhook } from '@/types/webhook';
/**
* Webhook API
* Implémente les endpoints webhooks selon le backend
* Endpoints:
* - POST /webhooks (protected) - Enregistrer un webhook
* - GET /webhooks (protected) - Lister les webhooks de l'utilisateur
* - DELETE /webhooks/:id (protected) - Supprimer un webhook
* - GET /webhooks/stats (protected) - Statistiques des webhooks
* - POST /webhooks/:id/test (protected) - Tester un webhook
* - POST /webhooks/:id/regenerate-key (protected) - Régénérer la clé API
*/
export interface RegisterWebhookRequest {
url: string;
events: string[]; // Array of event types (e.g., ['track.uploaded', 'user.created'])
}
export interface WebhookStats {
queue_size: number;
workers: number;
max_retries: number;
}
export interface WebhookStatsResponse {
user_id: string;
stats: WebhookStats;
}
export interface RegenerateAPIKeyResponse {
api_key: string;
message: string;
}
/**
* Enregistre un nouveau webhook
* @param data Données du webhook (URL et événements)
* @returns Le webhook créé
*/
export async function registerWebhook(
data: RegisterWebhookRequest,
): Promise<Webhook> {
const response = await apiClient.post<Webhook>('/webhooks', data);
return response.data;
}
/**
* Liste les webhooks de l'utilisateur authentifié
* @returns Liste des webhooks
*/
export async function listWebhooks(): Promise<Webhook[]> {
const response = await apiClient.get<Webhook[]>('/webhooks');
return response.data;
}
/**
* Supprime un webhook
* @param id ID du webhook à supprimer
* @returns Message de confirmation
*/
export async function deleteWebhook(id: string): Promise<{ message: string }> {
const response = await apiClient.delete<{ message: string }>(
`/webhooks/${id}`,
);
return response.data;
}
/**
* Récupère les statistiques des webhooks
* @returns Statistiques du worker de webhooks
*/
export async function getWebhookStats(): Promise<WebhookStatsResponse> {
const response = await apiClient.get<WebhookStatsResponse>('/webhooks/stats');
return response.data;
}
/**
* Teste un webhook en envoyant un événement de test
* @param id ID du webhook à tester
* @returns Message de confirmation
*/
export async function testWebhook(id: string): Promise<{ message: string }> {
const response = await apiClient.post<{ message: string }>(
`/webhooks/${id}/test`,
);
return response.data;
}
/**
* Régénère la clé API d'un webhook
* @param id ID du webhook
* @returns Nouvelle clé API et message de confirmation
*/
export async function regenerateWebhookAPIKey(
id: string,
): Promise<RegenerateAPIKeyResponse> {
const response = await apiClient.post<RegenerateAPIKeyResponse>(
`/webhooks/${id}/regenerate-key`,
);
return response.data;
}

View file

@ -4,6 +4,7 @@
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { apiClient } from '@/services/api/client';
import { parseApiError } from '@/utils/apiErrorHandler';
import { logger } from '@/utils/logger';
import type { ApiError } from '@/schemas/apiSchemas';
@ -17,15 +18,12 @@ export interface ValidationError {
value?: string;
}
/**
* Validation response from backend
*/
// ValidateResponse - used when backend validation endpoint is available
// interface ValidateResponse {
// valid: boolean;
// errors?: ValidationError[];
// message?: string;
// }
/** Validation response from backend POST /api/v1/validate */
interface ValidateResponse {
valid: boolean;
errors?: ValidationError[];
message?: string;
}
/**
* Options for useFormValidation hook
@ -97,52 +95,26 @@ export function useFormValidation(
setError(null);
try {
// FIX: L'endpoint /validate n'existe pas sur le backend
// Désactiver temporairement la validation backend jusqu'à ce que l'endpoint soit implémenté
// TODO: Implémenter l'endpoint /api/v1/validate sur le backend ou utiliser une validation côté client uniquement
// Log seulement en mode debug pour éviter le spam dans la console
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG === 'true') {
logger.debug('[useFormValidation] Backend validation endpoint not available, skipping validation', {
type,
});
}
// Retourner true pour ne pas bloquer le formulaire
setErrors([]);
setIsValid(true);
return true;
/* DISABLED: Backend validation endpoint doesn't exist
const response = await apiClient.post<ValidateResponse>(
'/validate', // FIX: Remove /api/v1 prefix as apiClient already has baseURL
{
type,
data,
},
const response = await apiClient.post<{ data?: ValidateResponse } & ValidateResponse>(
'/validate',
{ type, data: _data },
);
// Only update state if this is still the latest validation
if (validationId !== validationIdRef.current) {
return false; // Validation was superseded
return false;
}
const validationResult = response.data;
const raw = response.data as { data?: ValidateResponse } & ValidateResponse;
const result = raw?.data ?? raw;
// Handle both wrapped and direct response formats
const result =
typeof validationResult === 'object' && 'data' in validationResult
? (validationResult as { data: ValidateResponse }).data
: validationResult;
if (result.valid) {
if (result?.valid) {
setErrors([]);
setIsValid(true);
return true;
} else {
setErrors(result.errors || []);
setIsValid(false);
return false;
}
*/
setErrors(result?.errors ?? []);
setIsValid(false);
return false;
} catch (err) {
// Only update state if this is still the latest validation
if (validationId !== validationIdRef.current) {

View file

@ -0,0 +1,187 @@
/**
* MSW handlers for admin/audit/dashboard endpoints
*/
import { http, HttpResponse } from 'msw';
export const handlersAdmin = [
http.get('*/api/v1/audit/logs', () => {
return HttpResponse.json({
success: true,
data: {
logs: [
{
id: 'log-1',
action: 'user.login',
user_id: 'user-1',
resource: 'auth',
details: { ip: '127.0.0.1' },
timestamp: '2024-01-01T00:00:00Z',
user: { id: 'user-1', username: 'TestUser' },
},
{
id: 'log-2',
action: 'track.create',
user_id: 'user-1',
resource: 'track',
details: { track_id: 'track-1' },
timestamp: '2024-01-02T00:00:00Z',
user: { id: 'user-1', username: 'TestUser' },
},
],
total: 2,
page: 1,
limit: 20,
},
});
}),
http.get('*/api/v1/audit/stats', () => {
return HttpResponse.json({
success: true,
data: {
total_users: 12500,
total_revenue: 45000,
active_sessions: 1200,
pending_reports: 8,
trends: { users: 5, revenue: 10, sessions: -2, reports: 0 },
},
});
}),
http.post('*/api/v1/logs/frontend', () => {
return HttpResponse.json({ success: true });
}),
http.get('*/api/v1/dashboard', () => {
return HttpResponse.json({
success: true,
data: {
stats: {
tracks_played: 42,
messages_sent: 12,
favorites: 8,
active_friends: 3,
period: '30d',
},
recent_activity: [
{
id: 'act-1',
type: 'track_upload',
title: 'Track uploaded',
description: 'New track added',
timestamp: '2024-01-15T10:00:00Z',
},
],
library_preview: {
items: [],
total_count: 0,
has_more: false,
},
},
});
}),
http.get('*/api/v1/sessions/stats', () => {
return HttpResponse.json({
success: true,
data: {
user_id: 'user-1',
stats: { total_active: 1, unique_users: 1 },
},
});
}),
http.get('*/api/v1/roles', () => {
return HttpResponse.json({
success: true,
data: {
items: [
{ id: '1', name: 'admin', description: 'Administrator' },
{ id: '2', name: 'user', description: 'User' },
],
pagination: { total: 2, page: 1, limit: 20, total_pages: 1 },
},
});
}),
http.get('*/api/v1/roles/:id', () => {
return HttpResponse.json({
success: true,
data: { id: '1', name: 'admin', description: 'Administrator' },
});
}),
http.get('*/api/v1/users', () => {
return HttpResponse.json({
success: true,
data: {
items: [
{
id: 'user-1',
username: 'StorybookUser',
email: 'user@example.com',
role: 'admin',
avatar_url: 'https://i.pravatar.cc/150?u=1',
created_at: '2024-01-01T00:00:00Z',
status: 'active',
},
{
id: 'user-2',
username: 'AnotherUser',
email: 'user2@example.com',
role: 'user',
avatar_url: 'https://i.pravatar.cc/150?u=2',
created_at: '2024-01-02T00:00:00Z',
status: 'banned',
},
],
pagination: {
total: 2,
page: 1,
limit: 20,
total_pages: 1,
},
},
});
}),
http.get('*/api/v1/monitoring/metrics', () => {
return HttpResponse.json({
success: true,
data: {
cpu: { usage: 15, history: [10, 12, 15, 14, 15] },
memory: { usage: 45, total: 16000, history: [40, 42, 45, 44, 45] },
active_connections: 120,
requests_per_second: 50,
},
});
}),
http.get('*/api/v1/webhooks', () => {
return HttpResponse.json([
{
id: 'webhook-1',
url: 'https://example.com/webhook',
events: ['track.created', 'track.updated'],
active: true,
created_at: '2024-01-01T00:00:00Z',
},
]);
}),
http.get('*/api/v1/api-keys', () => {
return HttpResponse.json({
success: true,
data: [
{
id: 'key-1',
name: 'Production API Key',
key: 'pk_live_****************************',
created_at: '2024-01-01T00:00:00Z',
last_used: '2024-01-05T10:30:00Z',
},
],
});
}),
];

Some files were not shown because too many files have changed in this diff Show more