214 lines
7.1 KiB
Dart
214 lines
7.1 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:pluriwave/modelos/emisora.dart';
|
|
import 'package:pluriwave/servicios/servicio_grabacion_radio.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
group('ServicioGrabacionRadio', () {
|
|
test(
|
|
'guarda el stream original en disco con extensión por codec',
|
|
() async {
|
|
final dir = await Directory.systemTemp.createTemp('pluriwave-rec-');
|
|
final servicio = ServicioGrabacionRadio(
|
|
cliente: MockClient((request) async {
|
|
return http.Response.bytes(
|
|
[1, 2, 3, 4, 5],
|
|
200,
|
|
headers: {'content-type': 'audio/mpeg'},
|
|
);
|
|
}),
|
|
resolverDirectorioBase: () async => dir,
|
|
reloj: () => DateTime(2026, 5, 21, 18, 30),
|
|
);
|
|
|
|
await servicio.iniciar(
|
|
const Emisora(
|
|
uuid: 'r1',
|
|
nombre: 'Radio Prueba',
|
|
url: 'https://stream.example/radio',
|
|
codec: 'MP3',
|
|
),
|
|
);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
final carpeta = Directory(
|
|
'${dir.path}${Platform.pathSeparator}grabaciones',
|
|
);
|
|
final archivos = await carpeta.list().where((e) => e is File).toList();
|
|
expect(archivos, hasLength(1));
|
|
expect(archivos.single.path, endsWith('.mp3'));
|
|
expect(await File(archivos.single.path).readAsBytes(), [1, 2, 3, 4, 5]);
|
|
expect(servicio.estado.tipo, EstadoGrabacionRadioTipo.inactiva);
|
|
expect(servicio.ultimoArchivo?.path, archivos.single.path);
|
|
|
|
await servicio.dispose();
|
|
},
|
|
);
|
|
|
|
test('detiene una grabación activa bajo demanda', () async {
|
|
final dir = await Directory.systemTemp.createTemp('pluriwave-rec-stop-');
|
|
final controller = StreamController<List<int>>();
|
|
final servicio = ServicioGrabacionRadio(
|
|
cliente: _StreamClient(controller.stream),
|
|
resolverDirectorioBase: () async => dir,
|
|
);
|
|
|
|
await servicio.iniciar(
|
|
const Emisora(
|
|
uuid: 'r2',
|
|
nombre: 'Radio Larga',
|
|
url: 'https://stream.example/live',
|
|
codec: 'aac',
|
|
),
|
|
);
|
|
controller.add([10, 20, 30]);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(servicio.estado.activa, isTrue);
|
|
expect(servicio.estado.bytes, 3);
|
|
|
|
await servicio.detener();
|
|
|
|
expect(servicio.estado.tipo, EstadoGrabacionRadioTipo.inactiva);
|
|
expect(servicio.ultimoArchivo, isNotNull);
|
|
await controller.close();
|
|
await servicio.dispose();
|
|
});
|
|
|
|
test('se detiene automáticamente al alcanzar el tamaño máximo', () async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
final dir = await Directory.systemTemp.createTemp('pluriwave-rec-max-');
|
|
final controller = StreamController<List<int>>();
|
|
final servicio = ServicioGrabacionRadio(
|
|
cliente: _StreamClient(controller.stream),
|
|
resolverDirectorioBase: () async => dir,
|
|
);
|
|
|
|
await servicio.inicializar();
|
|
await servicio.guardarMaxBytes(3);
|
|
await servicio.iniciar(
|
|
const Emisora(
|
|
uuid: 'r3',
|
|
nombre: 'Radio Corta',
|
|
url: 'https://stream.example/short',
|
|
codec: 'mp3',
|
|
),
|
|
);
|
|
controller.add([1, 2, 3]);
|
|
await Future<void>.delayed(Duration.zero);
|
|
await Future<void>.delayed(const Duration(milliseconds: 20));
|
|
|
|
expect(servicio.estado.tipo, EstadoGrabacionRadioTipo.inactiva);
|
|
await controller.close();
|
|
await servicio.dispose();
|
|
});
|
|
|
|
// T-S6-05-A: a stream error triggers _fallar, which must clear active state
|
|
// and set status to error (S7-R5 invariant: no reconnect, immediate clear).
|
|
test('error de stream llama _fallar: estado pasa a error y no queda activa '
|
|
'(T-S6-05-A)', () async {
|
|
final dir = await Directory.systemTemp.createTemp('pluriwave-rec-err-');
|
|
final errorController = StreamController<List<int>>();
|
|
final servicio = ServicioGrabacionRadio(
|
|
cliente: _StreamClient(errorController.stream),
|
|
resolverDirectorioBase: () async => dir,
|
|
);
|
|
|
|
await servicio.iniciar(
|
|
const Emisora(
|
|
uuid: 'r4',
|
|
nombre: 'Radio Error',
|
|
url: 'https://stream.example/bad',
|
|
),
|
|
);
|
|
// Emit a byte so the stream is in "grabando" state, then error.
|
|
errorController.add([1]);
|
|
await Future<void>.delayed(Duration.zero);
|
|
errorController.addError(Exception('network failure'));
|
|
await Future<void>.delayed(const Duration(milliseconds: 20));
|
|
|
|
expect(
|
|
servicio.estado.activa,
|
|
isFalse,
|
|
reason: '_fallar must clear activa flag immediately',
|
|
);
|
|
expect(
|
|
servicio.estado.tipo,
|
|
EstadoGrabacionRadioTipo.error,
|
|
reason: '_fallar sets status to error, not inactiva',
|
|
);
|
|
expect(servicio.estado.error, contains('network failure'));
|
|
|
|
await errorController.close();
|
|
await servicio.dispose();
|
|
});
|
|
|
|
// T-S6-05-B: after an error, a subsequent iniciar call must succeed because
|
|
// _fallar resets all internal state (subscriptions, sink, client).
|
|
test('tras un error, iniciar de nuevo tiene exito (T-S6-05-B)', () async {
|
|
final dir = await Directory.systemTemp.createTemp('pluriwave-rec-retry-');
|
|
final errorController = StreamController<List<int>>();
|
|
final servicio = ServicioGrabacionRadio(
|
|
cliente: _StreamClient(errorController.stream),
|
|
resolverDirectorioBase: () async => dir,
|
|
);
|
|
|
|
// First attempt — error.
|
|
await servicio.iniciar(
|
|
const Emisora(
|
|
uuid: 'r5',
|
|
nombre: 'Radio Retry',
|
|
url: 'https://stream.example/retry',
|
|
),
|
|
);
|
|
errorController.addError(Exception('transient error'));
|
|
await Future<void>.delayed(const Duration(milliseconds: 20));
|
|
await errorController.close();
|
|
|
|
expect(servicio.estado.tipo, EstadoGrabacionRadioTipo.error);
|
|
|
|
// Second attempt with a clean stream — must not throw StateError.
|
|
final okController = StreamController<List<int>>();
|
|
final servicio2 = ServicioGrabacionRadio(
|
|
cliente: _StreamClient(okController.stream),
|
|
resolverDirectorioBase: () async => dir,
|
|
);
|
|
await expectLater(
|
|
servicio2.iniciar(
|
|
const Emisora(
|
|
uuid: 'r5',
|
|
nombre: 'Radio Retry',
|
|
url: 'https://stream.example/retry',
|
|
),
|
|
),
|
|
completes,
|
|
reason: 'after error state, a fresh service iniciar must not throw',
|
|
);
|
|
|
|
await okController.close();
|
|
await servicio.dispose();
|
|
await servicio2.dispose();
|
|
});
|
|
});
|
|
}
|
|
|
|
class _StreamClient extends http.BaseClient {
|
|
_StreamClient(this.stream);
|
|
|
|
final Stream<List<int>> stream;
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
|
return http.StreamedResponse(
|
|
stream,
|
|
200,
|
|
headers: {'content-type': 'audio/aac'},
|
|
);
|
|
}
|
|
}
|