veza/tests/e2e/12-api.spec.ts

222 lines
8 KiB
TypeScript
Raw Normal View History

import { test, expect } from '@playwright/test';
import { CONFIG } from './helpers';
/**
* Tests API directs verifient que le backend repond correctement
* independamment du frontend.
*
* API URL uses CONFIG.apiURL which defaults to http://localhost:5173
* (proxied through Vite in dev).
*
* Login response format:
* { success: true, data: { user: {...}, token: { access_token, expires_in } } }
*
* Error response format:
* { error: { code: 401, message: "Invalid credentials" } }
*/
test.describe('API — Health & Infrastructure', () => {
test('01. GET /api/v1/health renvoie 200 @critical', async ({ request }) => {
const response = await request.get(`${CONFIG.apiURL}/api/v1/health`);
expect(response.status()).toBe(200);
});
test('02. GET /api/v1/health/deep verifie toute l\'infra', async ({ request }) => {
const response = await request.get(`${CONFIG.apiURL}/api/v1/health/deep`);
console.log(` Health deep: ${response.status()}`);
if (response.ok()) {
const data = await response.json();
console.log(` Details: ${JSON.stringify(data).slice(0, 200)}`);
}
});
test('03. Stream server /health renvoie 200', async ({ request }) => {
try {
const response = await request.get(`${CONFIG.streamURL}/health`);
expect(response.status()).toBe(200);
} catch {
console.log(' Stream server inaccessible (http://localhost:18082)');
}
});
});
test.describe('API — Auth endpoints', () => {
test('04. POST /auth/login avec bons identifiants -> 200 + access_token', async ({ request }) => {
const response = await request.post(`${CONFIG.apiURL}/api/v1/auth/login`, {
data: {
email: CONFIG.users.listener.email,
password: CONFIG.users.listener.password,
},
});
expect(response.status()).toBe(200);
const body = await response.json();
// Response: { success: true, data: { user, token: { access_token, expires_in } } }
expect(body.success).toBe(true);
expect(body.data).toBeTruthy();
expect(body.data.token).toBeTruthy();
expect(body.data.token.access_token).toBeTruthy();
});
test('05. POST /auth/login avec mauvais identifiants -> 401', async ({ request }) => {
const response = await request.post(`${CONFIG.apiURL}/api/v1/auth/login`, {
data: {
email: CONFIG.users.listener.email,
password: 'wrong-password',
},
});
expect(response.status()).toBe(401);
const body = await response.json();
// Error response: { error: { code: 401, message: "Invalid credentials" } }
expect(body.error).toBeTruthy();
});
test('06. Acces endpoint protege sans token -> 401', async ({ request }) => {
const response = await request.get(`${CONFIG.apiURL}/api/v1/auth/me`);
const status = response.status();
console.log(` /auth/me without token: ${status}`);
// Accept 401 (Unauthorized), 403 (Forbidden), 302 (redirect), or 429 (rate limited)
expect([401, 403, 302, 429]).toContain(status);
});
test('07. Acces endpoint protege avec token valide -> 200', async ({ request }) => {
// Login first
const loginResponse = await request.post(`${CONFIG.apiURL}/api/v1/auth/login`, {
data: {
email: CONFIG.users.listener.email,
password: CONFIG.users.listener.password,
},
});
if (!loginResponse.ok()) {
console.log(` Login failed: ${loginResponse.status()} — skip`);
return;
}
const loginBody = await loginResponse.json();
const token = loginBody?.data?.token?.access_token;
if (!token) {
console.log(' Pas de token recu — skip');
return;
}
const response = await request.get(`${CONFIG.apiURL}/api/v1/auth/me`, {
headers: { Authorization: `Bearer ${token}` },
});
// Accept 200, 204 (no content), or 401/403 if token expired/invalid
const status = response.status();
console.log(` /auth/me with token: ${status}`);
expect([200, 204, 401, 403]).toContain(status);
});
});
test.describe('API — Endpoints principaux', () => {
let token: string;
test.beforeAll(async ({ request }) => {
const loginResponse = await request.post(`${CONFIG.apiURL}/api/v1/auth/login`, {
data: {
email: CONFIG.users.listener.email,
password: CONFIG.users.listener.password,
},
});
const body = await loginResponse.json();
token = body?.data?.token?.access_token || '';
});
// Verified endpoints from the actual Go routes:
// routes_auth.go: GET /api/v1/auth/me (protected)
// routes_tracks.go: GET /api/v1/tracks (public with optional auth)
// routes_playlists.go: GET /api/v1/playlists (protected)
// routes_core.go: GET /api/v1/notifications (protected)
// routes_feed.go: GET /api/v1/feed (protected)
// routes_social.go: GET /api/v1/social/feed (optional auth)
// routes_discover.go: GET /api/v1/discover/genres (public)
// routes_search.go: GET /api/v1/search?q=test (public)
// routes_marketplace.go: GET /api/v1/marketplace/products (public)
// routes_subscription.go: GET /api/v1/subscriptions/plans (public)
const endpoints = [
{ method: 'GET', path: '/api/v1/auth/me', name: 'Mon profil', auth: true },
{ method: 'GET', path: '/api/v1/tracks', name: 'Liste tracks', auth: true },
{ method: 'GET', path: '/api/v1/playlists', name: 'Mes playlists', auth: true },
{ method: 'GET', path: '/api/v1/notifications', name: 'Notifications', auth: true },
{ method: 'GET', path: '/api/v1/feed', name: 'Feed chronologique', auth: true },
{ method: 'GET', path: '/api/v1/social/feed', name: 'Social feed', auth: true },
{ method: 'GET', path: '/api/v1/discover/genres', name: 'Genres', auth: false },
{ method: 'GET', path: '/api/v1/search?q=test', name: 'Recherche', auth: false },
{ method: 'GET', path: '/api/v1/marketplace/products', name: 'Marketplace', auth: false },
{ method: 'GET', path: '/api/v1/subscriptions/plans', name: 'Plans abonnement', auth: false },
];
for (const endpoint of endpoints) {
test(`08. ${endpoint.method} ${endpoint.name} -> reponse valide`, async ({ request }) => {
if (endpoint.auth && !token) {
console.log(' Pas de token — skip');
return;
}
const headers: Record<string, string> = {};
if (endpoint.auth && token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await request.fetch(`${CONFIG.apiURL}${endpoint.path}`, {
method: endpoint.method,
headers,
});
const status = response.status();
console.log(` ${endpoint.name}: ${status}`);
// Must return 200 or 204 (not 500, 502, 503)
expect(status).toBeLessThan(500);
if (response.ok()) {
const body = await response.json().catch(() => null);
if (body) {
// Response must be valid JSON
expect(body).toBeTruthy();
}
}
});
}
});
test.describe('API — CORS et securite', () => {
test('09. CORS headers presents', async ({ request }) => {
const response = await request.fetch(`${CONFIG.apiURL}/api/v1/health`, {
method: 'OPTIONS',
headers: {
'Origin': 'http://localhost:5173',
'Access-Control-Request-Method': 'GET',
},
});
const corsHeader = response.headers()['access-control-allow-origin'];
console.log(` CORS Allow-Origin: ${corsHeader || 'absent'}`);
});
test('10. Rate limiting fonctionne (ne crash pas apres beaucoup de requetes)', async ({ request }) => {
const results: number[] = [];
for (let i = 0; i < 20; i++) {
const response = await request.get(`${CONFIG.apiURL}/api/v1/health`);
results.push(response.status());
}
const errors = results.filter(s => s >= 500);
console.log(` 20 requetes rapides: ${errors.length} erreurs serveur`);
expect(errors.length).toBe(0);
// 429 (rate limited) is normal and expected
const rateLimited = results.filter(s => s === 429);
if (rateLimited.length > 0) {
console.log(` Rate limiting actif: ${rateLimited.length} requetes bloquees`);
}
});
});