import 'dart:async'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:plezy/utils/media_server_http_client.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/services/base_shared_preferences_service.dart'; import 'package:plezy/services/update_service.dart'; import '../test_helpers/prefs.dart'; void main() { const lastCheckKey = 'update_last_check_time'; setUp(resetSharedPreferencesForTest); PackageInfo.setMockInitialValues( appName: 'Plezy', packageName: 'com.plezy.test', version: '1.0.0', buildNumber: '1', buildSignature: '', ); test('malformed cooldown state fails open and removes the invalid value', () async { final prefs = await BaseSharedPreferencesService.sharedCache(); await prefs.setString(lastCheckKey, 'not-an-instant'); expect(await UpdateService.shouldCheckForUpdates(), isTrue); expect(prefs.getString(lastCheckKey), isNull); }); test('future cooldown state fails open and removes the invalid value', () async { final prefs = await BaseSharedPreferencesService.sharedCache(); await prefs.setString(lastCheckKey, DateTime.now().add(const Duration(days: 30)).toIso8601String()); expect(await UpdateService.shouldCheckForUpdates(), isTrue); expect(prefs.getString(lastCheckKey), isNull); }); test('recent valid cooldown state suppresses a duplicate check and remains stored', () async { final prefs = await BaseSharedPreferencesService.sharedCache(); final recent = DateTime.now().subtract(const Duration(minutes: 5)).toIso8601String(); await prefs.setString(lastCheckKey, recent); expect(await UpdateService.shouldCheckForUpdates(), isFalse); expect(prefs.getString(lastCheckKey), recent); }); test('old valid cooldown state permits a new check', () async { final prefs = await BaseSharedPreferencesService.sharedCache(); final old = DateTime.now().subtract(const Duration(days: 2)).toIso8601String(); await prefs.setString(lastCheckKey, old); expect(await UpdateService.shouldCheckForUpdates(), isTrue); expect(prefs.getString(lastCheckKey), old); }); final failedResponses = Function()>{ 'timeout': () async => throw TimeoutException('request timed out'), 'non-200 response': () async => http.Response('unavailable', 503), 'parse failure': () async => http.Response('not-json', 200, headers: {'content-type': 'application/json'}), }; for (final failure in failedResponses.entries) { test('startup ${failure.key} records cooldown before request and manual check bypasses it', () async { final prefs = await BaseSharedPreferencesService.sharedCache(); final cooldownAtRequest = []; var requestCount = 0; final client = MediaServerHttpClient( client: MockClient((_) async { requestCount++; cooldownAtRequest.add(prefs.getString(lastCheckKey)); return failure.value(); }), ); addTearDown(client.close); expect(await UpdateService.debugPerformUpdateCheck(respectCooldown: true, client: client), isNull); expect(requestCount, 1); expect(cooldownAtRequest.single, isNotNull); final recordedCooldown = prefs.getString(lastCheckKey); expect(recordedCooldown, cooldownAtRequest.single); expect(DateTime.now().difference(DateTime.parse(recordedCooldown!)), lessThan(const Duration(minutes: 1))); expect(await UpdateService.debugPerformUpdateCheck(respectCooldown: true, client: client), isNull); expect(requestCount, 1, reason: 'a simulated next launch must honor the failed attempt cooldown'); expect(await UpdateService.debugPerformUpdateCheck(respectCooldown: false, client: client), isNull); expect(requestCount, 2, reason: 'an explicit manual check must bypass a recent startup cooldown'); expect( prefs.getString(lastCheckKey), recordedCooldown, reason: 'manual checks must not rewrite startup cooldown', ); }); } }