import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:pluriwave/servicios/servicio_radio.dart'; void main() { group('ServicioRadio retry + rotación', () { test( 'reintenta con otro host cuando el primero falla y recupera en el segundo', () async { final hostsSolicitados = []; final servicio = ServicioRadio( cliente: MockClient((request) async { hostsSolicitados.add(request.url.host); if (request.url.host == 'host-1.api.radio-browser.info') { return http.Response('fallo', 500); } return http.Response( jsonEncode([ { 'stationuuid': 'uuid-ok', 'name': 'Radio Recuperada', 'url_resolved': 'https://stream.recuperada/audio', }, ]), 200, headers: {'content-type': 'application/json'}, ); }), servidores: const [ 'host-1.api.radio-browser.info', 'host-2.api.radio-browser.info', ], maxIntentos: 3, retryDelay: Duration.zero, ); final emisoras = await servicio.obtenerPopulares(limit: 1); expect(emisoras, hasLength(1)); expect(emisoras.first.uuid, 'uuid-ok'); expect( hostsSolicitados, equals([ 'host-1.api.radio-browser.info', 'host-2.api.radio-browser.info', ]), ); }, ); test('corta al llegar al tope de intentos y propaga error final', () async { var intentos = 0; final servicio = ServicioRadio( cliente: MockClient((request) async { intentos += 1; throw http.ClientException('sin red', request.url); }), servidores: const ['host-unico.api.radio-browser.info'], maxIntentos: 2, retryDelay: Duration.zero, ); await expectLater( servicio.obtenerPopulares(limit: 1), throwsA(isA()), ); expect(intentos, 2); }); test('prioriza emisoras verificadas de mayor bitrate', () async { final servicio = ServicioRadio( cliente: MockClient((request) async { expect(request.url.queryParameters['order'], 'bitrate'); expect(request.url.queryParameters['reverse'], 'true'); return http.Response( jsonEncode([ { 'stationuuid': 'baja', 'name': 'Baja', 'url_resolved': 'https://stream.example/low', 'bitrate': 64, 'votes': 999, }, { 'stationuuid': 'alta', 'name': 'Alta', 'url_resolved': 'https://stream.example/high', 'bitrate': 320, 'votes': 1, }, ]), 200, headers: {'content-type': 'application/json'}, ); }), servidores: const ['host.api.radio-browser.info'], retryDelay: Duration.zero, ); final emisoras = await servicio.buscar(nombre: 'radio'); expect(emisoras.map((e) => e.uuid), equals(['alta', 'baja'])); }); }); // --------------------------------------------------------------------------- // Server discovery // // The API docs are explicit: "Never use a direct link to a single new server. // It is much better to get a list of the servers", because mirror names come // and go. Two of the three names this client used to hardcode (nl1, at1) no // longer resolve, which turned both retries into guaranteed failures. // --------------------------------------------------------------------------- group('ServicioRadio — User-Agent', () { test('identifica al cliente sin inventarse una versión', () async { String? agente; final servicio = ServicioRadio( cliente: MockClient((request) async { agente = request.headers['User-Agent']; return http.Response(jsonEncode(const []), 200); }), servidores: const ['host.api.radio-browser.info'], retryDelay: Duration.zero, ); await servicio.obtenerPopulares(limit: 1); expect(agente, isNotNull); expect(agente, contains('PluriWave')); // The version comes from the build at runtime; CI bumps it on every // release, so a literal here would be stale within one build. It used to // claim 0.1.0 while the app shipped 1.1.x. expect(agente, isNot(contains('0.1.0'))); }); }); group('ServicioRadio — descubrimiento de servidores', () { List respuestaEmisoras() => utf8.encode( jsonEncode([ { 'stationuuid': 'uuid-ok', 'name': 'Radio OK', 'url_resolved': 'https://stream.ok/audio', }, ]), ); test('no arranca con hosts muertos: usa el alias round-robin', () { expect( ServicioRadio.servidoresSemilla.first, equals('all.api.radio-browser.info'), ); expect( ServicioRadio.servidoresSemilla, isNot(contains('nl1.api.radio-browser.info')), ); expect( ServicioRadio.servidoresSemilla, isNot(contains('at1.api.radio-browser.info')), ); }); test('consulta /json/servers y usa los nombres descubiertos', () async { final hosts = []; final servicio = ServicioRadio( cliente: MockClient((request) async { hosts.add(request.url.host); if (request.url.path == '/json/servers') { return http.Response( jsonEncode([ {'ip': '1.2.3.4', 'name': 'descubierto.api.radio-browser.info'}, {'ip': '::1', 'name': 'descubierto.api.radio-browser.info'}, ]), 200, headers: {'content-type': 'application/json'}, ); } return http.Response.bytes(respuestaEmisoras(), 200); }), retryDelay: Duration.zero, ); final emisoras = await servicio.obtenerPopulares(limit: 1); expect(emisoras, hasLength(1)); expect(hosts.first, equals('all.api.radio-browser.info')); // Duplicate names (one per IP family) collapse to a single host. expect(hosts.sublist(1), equals(['descubierto.api.radio-browser.info'])); }); test('descubrimiento fallido cae a la semilla sin romper', () async { final hosts = []; final servicio = ServicioRadio( cliente: MockClient((request) async { hosts.add(request.url.host); if (request.url.path == '/json/servers') { throw http.ClientException('sin red', request.url); } return http.Response.bytes(respuestaEmisoras(), 200); }), retryDelay: Duration.zero, ); final emisoras = await servicio.obtenerPopulares(limit: 1); expect(emisoras, hasLength(1)); expect(hosts.last, isIn(ServicioRadio.servidoresSemilla)); }); test('el descubrimiento ocurre una sola vez por instancia', () async { var llamadasServers = 0; final servicio = ServicioRadio( cliente: MockClient((request) async { if (request.url.path == '/json/servers') { llamadasServers += 1; return http.Response( jsonEncode([ {'ip': '1.2.3.4', 'name': 'uno.api.radio-browser.info'}, ]), 200, ); } return http.Response.bytes(respuestaEmisoras(), 200); }), retryDelay: Duration.zero, ); await servicio.obtenerPopulares(limit: 1); await servicio.obtenerPopulares(limit: 1); expect(llamadasServers, equals(1)); }); test('servidores inyectados desactivan el descubrimiento', () async { var llamadasServers = 0; final servicio = ServicioRadio( cliente: MockClient((request) async { if (request.url.path == '/json/servers') { llamadasServers += 1; } return http.Response.bytes(respuestaEmisoras(), 200); }), servidores: const ['host.api.radio-browser.info'], retryDelay: Duration.zero, ); await servicio.obtenerPopulares(limit: 1); expect(llamadasServers, equals(0)); }); }); }