From 9384bd611787cba1c8452a558e6c5214249bbc59 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Sun, 12 Jul 2026 20:08:32 +0200 Subject: [PATCH] test: colma i buchi negli unit test esistenti MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - UpdateService.checkUpdate() coperto via HttpOverrides con fake HttpClient: versione più recente/uguale/più vecchia, status != 200, body malformato, errore di rete - ConverterProvider.loadDb() con l'asset reale + validazione che ogni voce di conversions.json abbia yield positivo - MealPlannerProvider.removeItem() sui rami colazione e cena (prima era coperto solo pranzo) Co-Authored-By: Claude Sonnet 5 --- .../providers/converter_provider_test.dart | 44 ++++++++ .../providers/meal_planner_provider_test.dart | 18 +++ test/shared/services/update_service_test.dart | 106 ++++++++++++++++++ 3 files changed, 168 insertions(+) diff --git a/test/features/converter/providers/converter_provider_test.dart b/test/features/converter/providers/converter_provider_test.dart index f5bec36..0aa144a 100644 --- a/test/features/converter/providers/converter_provider_test.dart +++ b/test/features/converter/providers/converter_provider_test.dart @@ -1,8 +1,12 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:biteplan/features/converter/providers/converter_provider.dart'; import 'package:biteplan/features/converter/models/conversion_entry.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); group('ConverterProvider', () { late ConverterProvider provider; @@ -187,5 +191,45 @@ void main() { expect(notified, true); }); }); + + group('loadDb()', () { + test('carica il database e la ricerca trova alimenti noti', () async { + await provider.loadDb(); + provider.search('riso'); + expect(provider.results, isNotEmpty); + provider.search('pollo'); + expect(provider.results, isNotEmpty); + }); + + test('la ricerca è case-insensitive', () async { + await provider.loadDb(); + provider.search('RISO'); + expect(provider.results, isNotEmpty); + }); + + test('notifica i listener', () async { + var notified = false; + provider.addListener(() => notified = true); + await provider.loadDb(); + expect(notified, true); + }); + }); + }); + + group('conversions.json', () { + test('ogni metodo di ogni alimento ha uno yield positivo', () async { + final raw = + await rootBundle.loadString('assets/data/conversions.json'); + final json = jsonDecode(raw) as Map; + expect(json, isNotEmpty); + json.forEach((food, methods) { + (methods as Map).forEach((method, data) { + final yield_ = (data as Map)['yield']; + expect(yield_, isA(), reason: '$food/$method'); + expect((yield_ as num).toDouble(), greaterThan(0), + reason: '$food/$method'); + }); + }); + }); }); } diff --git a/test/features/meal_planner/providers/meal_planner_provider_test.dart b/test/features/meal_planner/providers/meal_planner_provider_test.dart index d53fddd..6f69ba0 100644 --- a/test/features/meal_planner/providers/meal_planner_provider_test.dart +++ b/test/features/meal_planner/providers/meal_planner_provider_test.dart @@ -60,6 +60,24 @@ void main() { expect(provider.plan.days['lunedi']!.pranzo, ['pasta', 'frutta']); }); + test('removeItem rimuove dallo slot colazione', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('lunedi', 'colazione', 'latte'); + provider.addItem('lunedi', 'colazione', 'caffè'); + provider.removeItem('lunedi', 'colazione', 0); + expect(provider.plan.days['lunedi']!.colazione, ['caffè']); + }); + + test('removeItem rimuove dallo slot cena', () async { + final provider = MealPlannerProvider(); + await provider.load(); + provider.addItem('lunedi', 'cena', 'pollo'); + provider.addItem('lunedi', 'cena', 'verdure'); + provider.removeItem('lunedi', 'cena', 1); + expect(provider.plan.days['lunedi']!.cena, ['pollo']); + }); + test('clearAll svuota tutti i giorni', () async { final provider = MealPlannerProvider(); await provider.load(); diff --git a/test/shared/services/update_service_test.dart b/test/shared/services/update_service_test.dart index 462e03e..79079e3 100644 --- a/test/shared/services/update_service_test.dart +++ b/test/shared/services/update_service_test.dart @@ -1,7 +1,50 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/core/constants/app_constants.dart'; import 'package:biteplan/shared/services/update_service.dart'; void main() { + group('UpdateService.checkUpdate', () { + tearDown(() => HttpOverrides.global = null); + + test('ritorna la versione quando il tag remoto è più recente', () async { + HttpOverrides.global = + _FakeHttpOverrides(body: '{"tag_name":"v99.0.0"}'); + expect(await UpdateService.checkUpdate(), '99.0.0'); + }); + + test('ritorna null quando il tag remoto è uguale alla versione corrente', + () async { + HttpOverrides.global = + _FakeHttpOverrides(body: '{"tag_name":"v$kAppVersion"}'); + expect(await UpdateService.checkUpdate(), isNull); + }); + + test('ritorna null quando il tag remoto è più vecchio', () async { + HttpOverrides.global = _FakeHttpOverrides(body: '{"tag_name":"v0.0.1"}'); + expect(await UpdateService.checkUpdate(), isNull); + }); + + test('ritorna null con status diverso da 200', () async { + HttpOverrides.global = _FakeHttpOverrides( + statusCode: 404, body: '{"message":"Not Found"}'); + expect(await UpdateService.checkUpdate(), isNull); + }); + + test('ritorna null con body malformato', () async { + HttpOverrides.global = _FakeHttpOverrides(body: 'non json'); + expect(await UpdateService.checkUpdate(), isNull); + }); + + test('ritorna null se la connessione fallisce', () async { + HttpOverrides.global = _FakeHttpOverrides(fail: true); + expect(await UpdateService.checkUpdate(), isNull); + }); + }); + group('UpdateService.parseTagName', () { test('strips v prefix from tag_name', () { expect(UpdateService.parseTagName('{"tag_name":"v2.1.0"}'), '2.1.0'); @@ -71,3 +114,66 @@ void main() { }); }); } + +// ── Fake HTTP per checkUpdate ──────────────────────────────────────────────── + +class _FakeHttpOverrides extends HttpOverrides { + _FakeHttpOverrides({this.statusCode = 200, this.body = '', this.fail = false}); + final int statusCode; + final String body; + final bool fail; + + @override + HttpClient createHttpClient(SecurityContext? context) => + _FakeHttpClient(statusCode: statusCode, body: body, fail: fail); +} + +class _FakeHttpClient extends Fake implements HttpClient { + _FakeHttpClient( + {required this.statusCode, required this.body, required this.fail}); + final int statusCode; + final String body; + final bool fail; + + @override + set connectionTimeout(Duration? value) {} + + @override + Future getUrl(Uri url) async { + if (fail) throw const SocketException('rete assente'); + return _FakeHttpClientRequest(statusCode: statusCode, body: body); + } + + @override + void close({bool force = false}) {} +} + +class _FakeHttpClientRequest extends Fake implements HttpClientRequest { + _FakeHttpClientRequest({required this.statusCode, required this.body}); + final int statusCode; + final String body; + + @override + HttpHeaders get headers => _FakeHttpHeaders(); + + @override + Future close() async => + _FakeHttpClientResponse(statusCode: statusCode, body: body); +} + +class _FakeHttpHeaders extends Fake implements HttpHeaders { + @override + void set(String name, Object value, {bool preserveHeaderCase = false}) {} +} + +class _FakeHttpClientResponse extends Fake implements HttpClientResponse { + _FakeHttpClientResponse({required this.statusCode, required this.body}); + + @override + final int statusCode; + final String body; + + @override + Stream transform(StreamTransformer, S> streamTransformer) => + Stream.value(utf8.encode(body)).transform(streamTransformer); +}