Refacto du code coté Python, java et coté angular afin de mieux séparer les responsabilité et d'avoir moins de répétitivité dans le code.
Some checks failed
E2E Tests / e2e (push) Waiting to run
Tests unitaires / Web (Angular · vitest + couverture) (push) Successful in 26s
Build & Push Images / tests (push) Failing after 13s
Build & Push Images / build (brain) (push) Has been skipped
Build & Push Images / build (core) (push) Has been skipped
Build & Push Images / build (web) (push) Has been skipped
Build & Push Images / build-switcher (push) Has been skipped
Tests unitaires / Brain (Python · pytest + couverture) (push) Successful in 1m12s
Tests unitaires / Core (Java · mvn test + JaCoCo) (push) Failing after 1m28s

Mise en place de tests unitaires coté Python et Angular
Mise en place de la couverture de test directement dans le workflow : le programme ne build pas si jamais un test échoue
Passage en v0.16.2 en conséquence
This commit is contained in:
2026-06-18 15:59:10 +02:00
parent eb78a75621
commit 4d049274f9
68 changed files with 4433 additions and 1360 deletions

View File

@@ -1,9 +1,10 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Observable, Subscriber } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { GameSystem, GameSystemCreate, RulesImportResponse, RulesImportStreamEvent } from './game-system.model';
import { LanguageService } from './language.service';
import { parseSseStream, sseFetch } from '../shared/sse.util';
/**
* Service HTTP pour les GameSystems (systèmes de JDR).
@@ -57,66 +58,46 @@ export class GameSystemService {
* subscription annule le fetch (AbortController).
*/
importRulesStream(file: File): Observable<RulesImportStreamEvent> {
return new Observable<RulesImportStreamEvent>((subscriber) => {
const controller = new AbortController();
const form = new FormData();
form.append('file', file);
fetch(`${this.apiUrl}/import-rules/stream`, {
const form = new FormData();
form.append('file', file);
return sseFetch<RulesImportStreamEvent>(
`${this.apiUrl}/import-rules/stream`,
{
method: 'POST',
headers: { 'Accept': 'text/event-stream', 'X-User-Language': this.language.current },
body: form,
signal: controller.signal
})
.then(async (response) => {
if (!response.ok || !response.body) {
subscriber.error(new Error(`HTTP ${response.status}`));
return;
}
await this.consumeImportSse(response.body, subscriber);
})
.catch((err) => {
if (controller.signal.aborted) return;
subscriber.error(err);
});
return () => controller.abort();
});
body: form
},
(body, subscriber) => this.consumeImportSse(body, subscriber)
);
}
/** Décode un ReadableStream SSE et émet les évènements d'import typés. */
/** Mappe les évènements SSE bruts vers les évènements d'import typés. */
private async consumeImportSse(
body: ReadableStream<Uint8Array>,
subscriber: { next: (e: RulesImportStreamEvent) => void; error: (e: unknown) => void; complete: () => void }
subscriber: Subscriber<RulesImportStreamEvent>
): Promise<void> {
const reader = body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let currentEvent: string | null = null;
let currentData = '';
// Le flux s'est-il terminé PROPREMENT (évènement done ou error reçu) ?
// Sans ce suivi, une connexion coupée en plein import (timeout serveur,
// proxy, Core redémarré) terminait l'Observable en silence : barre de
// progression figée et aucun message pour l'utilisateur.
let terminated = false;
const dispatch = () => {
const name = currentEvent ?? 'message';
if (name === 'error') {
const dispatch = ({ event, data }: { event: string; data: string }) => {
if (event === 'error') {
let message = this.translate.instant('services.importFailed');
try { message = (JSON.parse(currentData) as { message?: string }).message ?? message; } catch { /* garde le défaut */ }
try { message = (JSON.parse(data) as { message?: string }).message ?? message; } catch { /* garde le défaut */ }
terminated = true;
subscriber.error(new Error(message));
} else if (name === 'status') {
} else if (event === 'status') {
// Message d'attente lisible (fournisseur saturé, morceau re-découpé…).
try {
const obj = JSON.parse(currentData) as { message?: string };
const obj = JSON.parse(data) as { message?: string };
if (obj.message) subscriber.next({ type: 'status', message: obj.message });
} catch { /* bloc malformé ignoré */ }
} else if (name === 'progress' || name === 'done') {
} else if (event === 'progress' || event === 'done') {
try {
const obj = JSON.parse(currentData);
if (name === 'done') {
const obj = JSON.parse(data);
if (event === 'done') {
terminated = true;
subscriber.next({ type: 'done', ...obj });
subscriber.complete();
@@ -125,32 +106,10 @@ export class GameSystemService {
}
} catch { /* bloc malformé ignoré */ }
}
currentEvent = null;
currentData = '';
};
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let idx: number;
while ((idx = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, idx).replace(/\r$/, '');
buffer = buffer.slice(idx + 1);
if (line === '') {
if (currentEvent !== null || currentData !== '') dispatch();
continue;
}
if (line.startsWith('event:')) {
currentEvent = line.slice(6).trim();
} else if (line.startsWith('data:')) {
const chunk = line.slice(5).replace(/^ /, '');
currentData = currentData ? `${currentData}\n${chunk}` : chunk;
}
}
}
if (currentEvent !== null || currentData !== '') dispatch();
await parseSseStream(body, dispatch);
if (!terminated) {
subscriber.error(new Error(
this.translate.instant('services.importInterrupted')));