Files
xtremflow/bin/test/logo_catalog_test.dart
T
MichaelandClaude Opus 5 9843999809 Logos des chaînes : repli par nom quand l'hébergeur de picons tombe
L'hébergeur des picons du fournisseur (51.158.145.100) répond toujours
503 sur toutes ses URL. Couper le flot de requêtes ne suffisait pas : la
grille restait sans logo. Pire, le pixel transparent servi en 200 était
pris pour une image valide par Flutter, qui n'affichait plus son icône
de repli — d'où des tuiles entièrement vides.

- Nouvelle route `/api/logo?src=…&name=…` : URL du panneau d'abord (via
  le proxy, mêmes protections anti-SSRF et même mémoire des hôtes
  morts), puis recherche par nom dans le dépôt public tv-logo/tv-logos
- Appariement par nom : préfixe pays et suffixes de qualité ignorés,
  `+` → `plus`, variante plus longue (`bein-sports-1-french`), puis
  marque sans numéro pour les canaux événementiels. Aucune approximation
  sur un mot seul
- Liste de logos en cache 24 h (API GitHub : 60 requêtes/h non
  authentifiées), 15 min de recul après échec, images en mémoire
- Image indisponible : 410 mis en cache au lieu du pixel. 410 et non
  404 : la Cascade du serveur rattrape les 404 et renvoyait celui du
  handler statique, sans cache-control
- Client (grille bureau, mobile, enregistrements) : logo toujours
  demandé au backend, nom compris, même sans stream_icon. L'onglet
  Enregistrements chargeait l'URL http brute, bloquée en HTTPS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 13:39:01 +02:00

136 lines
4.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:test/test.dart';
import '../services/logo_catalog.dart';
/// Extrait réel de `tv-logo/tv-logos/countries/france` (noms sans `-fr.png`).
const _france = {
'bein-sports-1-french',
'bein-sports-2-french',
'bein-sports',
'canal-plus',
'canal-plus-sport',
'canal-plus-sport-360',
'eurosport-1',
'france-2',
'france-24',
'lequipe',
'rmc-sport-1',
'rmc-sport-2',
'rmc-sport-access-1',
};
void main() {
group('matchLogoKey', () {
String? match(String name) => matchLogoKey(name, _france);
test('correspondance exacte après nettoyage du nom panneau', () {
// Préfixe pays et suffixe de qualité sont du bruit propre au panneau.
expect(match('FR - RMC SPORT 2 FHD'), 'rmc-sport-2');
expect(match('FR: France 2 HD'), 'france-2');
expect(match('|FR| EUROSPORT 1 UHD'), 'eurosport-1');
});
test('traduit le « + » de Canal+', () {
expect(match('FR - CANAL+ SPORT FHD'), 'canal-plus-sport');
expect(match('FR - CANAL+'), 'canal-plus');
});
test('accepte une variante plus longue du même identifiant', () {
// Le dépôt nomme `bein-sports-1-french`, le panneau `BEIN SPORTS 1`.
expect(match('FR - BEIN SPORTS 1 FHD'), 'bein-sports-1-french');
});
test('retombe sur la marque quand le numéro de canal est inconnu', () {
// Les canaux événementiels (« Live 5 ») n'ont pas de logo propre :
// celui de la marque vaut mieux qu'une tuile vide.
expect(match('FR - RMC SPORT LIVE 5 FHD'), 'rmc-sport-1');
expect(match('FR - CANAL+ SPORT 2'), 'canal-plus-sport');
});
test('ne devine pas sur un seul mot', () {
// « France » seul ne doit pas donner le logo de France 24.
expect(match('FR - FRANCE'), isNull);
});
test('rend null quand rien ne correspond', () {
expect(match('FR - DAZN FHD'), isNull);
expect(match(''), isNull);
});
});
group('countryFolderFor', () {
test('lit le préfixe pays du nom', () {
expect(countryFolderFor('FR - TF1'), 'france');
expect(countryFolderFor('BE: RTL TVI'), 'belgium');
expect(countryFolderFor('|UK| BBC ONE'), 'united-kingdom');
});
test('France par défaut sans préfixe reconnu', () {
expect(countryFolderFor('TF1 HD'), 'france');
});
});
group('LogoCatalog', () {
test('télécharge le logo apparié et le garde en cache', () async {
final requested = <String>[];
final client = MockClient((request) async {
requested.add(request.url.toString());
if (request.url.host == 'api.github.com') {
return http.Response(
jsonEncode([
{
'name': 'rmc-sport-2-fr.png',
'download_url': 'https://raw.example/rmc-sport-2-fr.png',
},
{'name': 'README.md', 'download_url': 'https://raw.example/r'},
]),
200,
);
}
return http.Response.bytes([1, 2, 3], 200);
});
final catalog = LogoCatalog(client: client);
final first = await catalog.logoFor('FR - RMC SPORT 2 FHD');
final second = await catalog.logoFor('FR - RMC SPORT 2 FHD');
expect(first, [1, 2, 3]);
expect(second, [1, 2, 3]);
// Une liste et un logo, pas un aller-retour par affichage.
expect(requested, hasLength(2));
});
test('rend null sans correspondance, sans télécharger d’image', () async {
var downloads = 0;
final client = MockClient((request) async {
if (request.url.host == 'api.github.com') {
return http.Response(jsonEncode(const []), 200);
}
downloads++;
return http.Response.bytes([1], 200);
});
final catalog = LogoCatalog(client: client);
expect(await catalog.logoFor('FR - CHAINE INCONNUE'), isNull);
expect(downloads, 0);
});
test('espace les tentatives quand la liste est injoignable', () async {
var listings = 0;
final client = MockClient((request) async {
listings++;
return http.Response('rate limited', 403);
});
final catalog = LogoCatalog(client: client);
await catalog.logoFor('FR - RMC SPORT 2');
await catalog.logoFor('FR - RMC SPORT 1');
expect(listings, 1);
});
});
}