From b52ef152eb4ff13dd93eb30caac81c635afc9df6 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 12:32:21 +0200 Subject: [PATCH 01/12] feat: use branded api.pushfire.app domain as default base URL --- README.md | 2 +- lib/src/config/pushfire_config.dart | 2 +- test/config/pushfire_config_test.dart | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ce1abee..5b32282 100644 --- a/README.md +++ b/README.md @@ -498,7 +498,7 @@ if (PushFireSDK.isInitialized) { | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `apiKey` | String | Yes | - | Your PushFire API key | -| `baseUrl` | String | No | `https://jojnoebcqoqjlshwzmjm.supabase.co/functions/v1/` | API base URL | +| `baseUrl` | String | No | `https://api.pushfire.app/functions/v1/` | API base URL | | `enableLogging` | bool | No | `false` | Enable debug logging | | `timeoutSeconds` | int | No | `30` | Request timeout in seconds | | `authProvider` | AuthProvider | No | `AuthProvider.none` | Authentication provider for automatic subscriber management | diff --git a/lib/src/config/pushfire_config.dart b/lib/src/config/pushfire_config.dart index bbfe011..762e31b 100644 --- a/lib/src/config/pushfire_config.dart +++ b/lib/src/config/pushfire_config.dart @@ -20,7 +20,7 @@ class PushFireConfig { const PushFireConfig({ required this.apiKey, - this.baseUrl = 'https://jojnoebcqoqjlshwzmjm.supabase.co/functions/v1/', + this.baseUrl = 'https://api.pushfire.app/functions/v1/', this.enableLogging = false, this.timeoutSeconds = 30, this.authProvider = AuthProvider.none, diff --git a/test/config/pushfire_config_test.dart b/test/config/pushfire_config_test.dart index e5d6829..19b4be5 100644 --- a/test/config/pushfire_config_test.dart +++ b/test/config/pushfire_config_test.dart @@ -44,10 +44,10 @@ void main() { expect(config.apiKey, 'test-api-key'); }); - test('baseUrl defaults to the expected Supabase URL', () { + test('baseUrl defaults to the expected PushFire API URL', () { expect( config.baseUrl, - 'https://jojnoebcqoqjlshwzmjm.supabase.co/functions/v1/', + 'https://api.pushfire.app/functions/v1/', ); }); @@ -258,7 +258,7 @@ void main() { expect( result, 'PushFireConfig(baseUrl: ' - 'https://jojnoebcqoqjlshwzmjm.supabase.co/functions/v1/, ' + 'https://api.pushfire.app/functions/v1/, ' 'enableLogging: false, timeoutSeconds: 30)', ); }); From b01fa16f361e58ebc91a71f8d672b7ccb9821822 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 12:33:49 +0200 Subject: [PATCH 02/12] feat(logger): add logApiError for clear API failure logging --- lib/src/utils/logger.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/src/utils/logger.dart b/lib/src/utils/logger.dart index b77a12b..78ec603 100644 --- a/lib/src/utils/logger.dart +++ b/lib/src/utils/logger.dart @@ -84,6 +84,23 @@ class PushFireLogger { } } + /// Log an API error response as a single, clear error line + static void logApiError( + String method, + String endpoint, + int statusCode, + String? code, + String message, + ) { + if (_enableLogging) { + final buffer = + StringBuffer('API error: $method $endpoint -> HTTP $statusCode'); + if (code != null) buffer.write(' code=$code'); + buffer.write(' msg="$message"'); + error(buffer.toString()); + } + } + /// Log device information static void logDeviceInfo(Map deviceInfo) { if (_enableLogging) { From 4e87f813ea5385b6ae9fe983d8b4a49a52dc7834 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 12:37:26 +0200 Subject: [PATCH 03/12] fix(api): preserve structured API exceptions and label timeouts --- lib/src/api/pushfire_api_client.dart | 29 +++++-- test/api/pushfire_api_client_test.dart | 106 +++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 test/api/pushfire_api_client_test.dart diff --git a/lib/src/api/pushfire_api_client.dart b/lib/src/api/pushfire_api_client.dart index e8d8cf4..2ecacdc 100644 --- a/lib/src/api/pushfire_api_client.dart +++ b/lib/src/api/pushfire_api_client.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:http/http.dart' as http; @@ -10,8 +11,8 @@ class PushFireApiClient { final PushFireConfig _config; late final http.Client _httpClient; - PushFireApiClient(this._config) { - _httpClient = http.Client(); + PushFireApiClient(this._config, {http.Client? httpClient}) { + _httpClient = httpClient ?? http.Client(); } /// Get common headers for API requests @@ -86,21 +87,28 @@ class PushFireApiClient { response.body, ); - return _handleResponse(response); + return _handleResponse(method, endpoint, response); + } on PushFireException { + rethrow; + } on TimeoutException catch (e) { + final message = + 'Request to $method $endpoint timed out after ${_config.timeoutSeconds}s'; + PushFireLogger.error(message, e); + throw PushFireNetworkException(message, originalError: e); } on SocketException catch (e) { - PushFireLogger.error('Network error: ${e.message}', e); + PushFireLogger.error('Network error during $method $endpoint: ${e.message}', e); throw PushFireNetworkException( 'Network error: ${e.message}', originalError: e, ); } on HttpException catch (e) { - PushFireLogger.error('HTTP error: ${e.message}', e); + PushFireLogger.error('HTTP error during $method $endpoint: ${e.message}', e); throw PushFireNetworkException( 'HTTP error: ${e.message}', originalError: e, ); } catch (e) { - PushFireLogger.error('Unexpected error during API request', e); + PushFireLogger.error('Unexpected error during $method $endpoint', e); throw PushFireApiException( 'Unexpected error: $e', originalError: e, @@ -109,7 +117,11 @@ class PushFireApiClient { } /// Handle HTTP response - Map _handleResponse(http.Response response) { + Map _handleResponse( + String method, + String endpoint, + http.Response response, + ) { final statusCode = response.statusCode; final body = response.body; @@ -142,6 +154,9 @@ class PushFireApiClient { errorMessage = 'API request failed with status $statusCode'; } + PushFireLogger.logApiError( + method, endpoint, statusCode, errorCode, errorMessage); + throw PushFireApiException( errorMessage, code: errorCode, diff --git a/test/api/pushfire_api_client_test.dart b/test/api/pushfire_api_client_test.dart new file mode 100644 index 0000000..2a3493f --- /dev/null +++ b/test/api/pushfire_api_client_test.dart @@ -0,0 +1,106 @@ +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:pushfire_sdk/src/api/pushfire_api_client.dart'; +import 'package:pushfire_sdk/src/config/pushfire_config.dart'; +import 'package:pushfire_sdk/src/exceptions/pushfire_exceptions.dart'; + +void main() { + const config = PushFireConfig( + apiKey: 'test-key', + baseUrl: 'https://api.pushfire.app/functions/v1/', + ); + + group('PushFireApiClient success responses', () { + test('2xx with JSON body returns decoded map', () async { + final mock = MockClient((req) async => http.Response('{"id":"sub_1"}', 200)); + final client = PushFireApiClient(config, httpClient: mock); + final result = await client.post('login-subscriber', {'data': {}}); + expect(result['id'], 'sub_1'); + }); + + test('2xx with empty body returns success map', () async { + final mock = MockClient((req) async => http.Response('', 200)); + final client = PushFireApiClient(config, httpClient: mock); + final result = await client.post('logout-subscriber', {'data': {}}); + expect(result['success'], true); + }); + }); + + group('PushFireApiClient error responses', () { + test('4xx with JSON error preserves status, code, message, body', () async { + final mock = MockClient((req) async => http.Response( + '{"message":"Missing authorization header","code":"missing_auth"}', + 401, + )); + final client = PushFireApiClient(config, httpClient: mock); + + await expectLater( + client.patch('update-subscriber', {'data': {}}), + throwsA(isA() + .having((e) => e.statusCode, 'statusCode', 401) + .having((e) => e.code, 'code', 'missing_auth') + .having((e) => e.message, 'message', 'Missing authorization header') + .having((e) => e.responseBody, 'responseBody', + contains('Missing authorization header'))), + ); + }); + + test('error is NOT re-wrapped as "Unexpected error"', () async { + final mock = MockClient((req) async => http.Response('{"message":"Nope"}', 403)); + final client = PushFireApiClient(config, httpClient: mock); + + await expectLater( + client.post('x', {}), + throwsA(isA() + .having((e) => e.message, 'message', isNot(contains('Unexpected'))) + .having((e) => e.statusCode, 'statusCode', 403)), + ); + }); + + test('5xx sets the status code', () async { + final mock = MockClient((req) async => http.Response('{"message":"boom"}', 500)); + final client = PushFireApiClient(config, httpClient: mock); + await expectLater( + client.post('x', {}), + throwsA(isA() + .having((e) => e.statusCode, 'statusCode', 500)), + ); + }); + + test('non-JSON error body falls back to a status message', () async { + final mock = MockClient((req) async => http.Response('502', 502)); + final client = PushFireApiClient(config, httpClient: mock); + await expectLater( + client.post('x', {}), + throwsA(isA() + .having((e) => e.statusCode, 'statusCode', 502) + .having((e) => e.message, 'message', + 'API request failed with status 502')), + ); + }); + }); + + group('PushFireApiClient transport errors', () { + test('SocketException becomes PushFireNetworkException', () async { + final mock = MockClient((req) async => throw const SocketException('no route')); + final client = PushFireApiClient(config, httpClient: mock); + await expectLater( + client.post('x', {}), + throwsA(isA()), + ); + }); + + test('TimeoutException becomes PushFireNetworkException', () async { + final mock = MockClient((req) async => throw TimeoutException('slow')); + final client = PushFireApiClient(config, httpClient: mock); + await expectLater( + client.post('x', {}), + throwsA(isA()), + ); + }); + }); +} From 80f49594a5e853f0bb210652baed8fd035810024 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 12:40:57 +0200 Subject: [PATCH 04/12] test(api): tighten transport-error and non-JSON response assertions --- test/api/pushfire_api_client_test.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/api/pushfire_api_client_test.dart b/test/api/pushfire_api_client_test.dart index 2a3493f..616e42f 100644 --- a/test/api/pushfire_api_client_test.dart +++ b/test/api/pushfire_api_client_test.dart @@ -79,7 +79,8 @@ void main() { throwsA(isA() .having((e) => e.statusCode, 'statusCode', 502) .having((e) => e.message, 'message', - 'API request failed with status 502')), + 'API request failed with status 502') + .having((e) => e.responseBody, 'responseBody', contains('502'))), ); }); }); @@ -90,7 +91,8 @@ void main() { final client = PushFireApiClient(config, httpClient: mock); await expectLater( client.post('x', {}), - throwsA(isA()), + throwsA(isA() + .having((e) => e.message, 'message', contains('Network error'))), ); }); @@ -99,7 +101,8 @@ void main() { final client = PushFireApiClient(config, httpClient: mock); await expectLater( client.post('x', {}), - throwsA(isA()), + throwsA(isA() + .having((e) => e.message, 'message', contains('timed out'))), ); }); }); From 73c2f4f1ded6269f209a4945f4d515d6dfa622fd Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 12:44:25 +0200 Subject: [PATCH 05/12] refactor(services): log API failures once, drop duplicate vague logs --- lib/src/services/device_service.dart | 37 ++++++++++++------------ lib/src/services/subscriber_service.dart | 16 +++++----- lib/src/services/tag_service.dart | 21 ++++++-------- lib/src/services/workflow_service.dart | 7 ++--- 4 files changed, 38 insertions(+), 43 deletions(-) diff --git a/lib/src/services/device_service.dart b/lib/src/services/device_service.dart index 4934617..3355d82 100644 --- a/lib/src/services/device_service.dart +++ b/lib/src/services/device_service.dart @@ -115,11 +115,10 @@ class DeviceService { PushFireLogger.info( 'Device registration completed: ${registeredDevice.id}'); return registeredDevice; + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Device registration failed', e); - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error during device registration', e); throw PushFireDeviceException('Device registration failed: $e', originalError: e); } @@ -148,10 +147,10 @@ class DeviceService { } return device.copyWith(id: deviceId); + } on PushFireException { + rethrow; } catch (e) { - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error registering new device', e); throw PushFireDeviceException('Failed to register device: $e', originalError: e); } @@ -163,10 +162,10 @@ class DeviceService { final deviceData = {'data': device.toJson()}; await _apiClient.patch('update-device', deviceData); return device; + } on PushFireException { + rethrow; } catch (e) { - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error updating device', e); throw PushFireDeviceException('Failed to update device: $e', originalError: e); } @@ -395,9 +394,11 @@ class DeviceService { } return isGranted; + } on PushFireException { + rethrow; } catch (e) { PushFireLogger.error( - 'Failed to request notification permission manually', e); + 'Unexpected error requesting notification permission', e); throw PushFireDeviceException( 'Failed to request notification permission: $e', originalError: e); @@ -470,11 +471,10 @@ class DeviceService { PushFireLogger.info('Notification preference updated to $enabled'); return SetNotificationResult.success; + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Failed to set notification enabled', e); - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error setting notification preference', e); throw PushFireDeviceException( 'Failed to set notification preference: $e', originalError: e); @@ -494,9 +494,10 @@ class DeviceService { isPermissionGranted: osPermission, isEnabled: savedPreference ?? true, ); + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Failed to get notification status', e); - if (e is PushFireException) rethrow; + PushFireLogger.error('Unexpected error getting notification status', e); throw PushFireDeviceException('Failed to get notification status: $e', originalError: e); } @@ -566,7 +567,7 @@ class DeviceService { } } } catch (e) { - PushFireLogger.error('Failed to check permission status change', e); + PushFireLogger.warning('Failed to check permission status change', e); return null; } } diff --git a/lib/src/services/subscriber_service.dart b/lib/src/services/subscriber_service.dart index c1fd602..ac189ec 100644 --- a/lib/src/services/subscriber_service.dart +++ b/lib/src/services/subscriber_service.dart @@ -86,11 +86,10 @@ class SubscriberService { PushFireLogger.info('Subscriber login completed: $subscriberId'); return subscriber; + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Subscriber login failed', e); - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error during subscriber login', e); throw PushFireSubscriberException( 'Subscriber login failed: $e', originalError: e, @@ -126,11 +125,10 @@ class SubscriberService { await _apiClient.patch('update-subscriber', updateData); PushFireLogger.info('Subscriber updated successfully'); + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Subscriber update failed', e); - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error during subscriber update', e); throw PushFireSubscriberException( 'Subscriber update failed: $e', originalError: e, @@ -168,13 +166,13 @@ class SubscriberService { PushFireLogger.info('Subscriber logout completed'); } catch (e) { - PushFireLogger.error('Subscriber logout failed', e); // Clear local data even if API call fails await _clearSubscriberData(); if (e is PushFireException) { rethrow; } + PushFireLogger.error('Unexpected error during subscriber logout', e); throw PushFireSubscriberException( 'Subscriber logout failed: $e', originalError: e, diff --git a/lib/src/services/tag_service.dart b/lib/src/services/tag_service.dart index 64bb315..c2ad734 100644 --- a/lib/src/services/tag_service.dart +++ b/lib/src/services/tag_service.dart @@ -45,11 +45,10 @@ class TagService { PushFireLogger.info('Tag added successfully: $tagId'); return tag; + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Failed to add tag', e); - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error adding tag', e); throw PushFireTagException( 'Failed to add tag: $e', originalError: e, @@ -91,11 +90,10 @@ class TagService { PushFireLogger.info('Tag updated successfully: $tagId'); return tag; + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Failed to update tag', e); - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error updating tag', e); throw PushFireTagException( 'Failed to update tag: $e', originalError: e, @@ -128,11 +126,10 @@ class TagService { await _apiClient.delete('remove-subscriber-tag', tagData); PushFireLogger.info('Tag removed successfully: $tagId'); + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Failed to remove tag', e); - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error removing tag', e); throw PushFireTagException( 'Failed to remove tag: $e', originalError: e, diff --git a/lib/src/services/workflow_service.dart b/lib/src/services/workflow_service.dart index 1c4e80d..456f125 100644 --- a/lib/src/services/workflow_service.dart +++ b/lib/src/services/workflow_service.dart @@ -32,11 +32,10 @@ class WorkflowService { PushFireLogger.info('Workflow execution created successfully'); return response; + } on PushFireException { + rethrow; } catch (e) { - PushFireLogger.error('Workflow execution creation failed', e); - if (e is PushFireException) { - rethrow; - } + PushFireLogger.error('Unexpected error creating workflow execution', e); throw PushFireApiException( 'Workflow execution creation failed: $e', originalError: e, From 41ec5803b98f082ac4601cd8783da4a76b717ad3 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 12:48:06 +0200 Subject: [PATCH 06/12] refactor(impl): drop duplicate rethrow logs, downgrade non-fatal logs to warning --- lib/src/pushfire_sdk_impl.dart | 115 ++++++++++++++------------------- 1 file changed, 48 insertions(+), 67 deletions(-) diff --git a/lib/src/pushfire_sdk_impl.dart b/lib/src/pushfire_sdk_impl.dart index 68e668e..618e5e1 100644 --- a/lib/src/pushfire_sdk_impl.dart +++ b/lib/src/pushfire_sdk_impl.dart @@ -111,9 +111,10 @@ class PushFireSDKImpl with WidgetsBindingObserver { await Firebase.initializeApp(); PushFireLogger.info('Firebase initialized successfully'); } catch (e) { - // Firebase might already be initialized, which is fine + // Firebase is most likely already initialized (common and harmless). + // If it is a real failure, downstream FCM calls will surface it. PushFireLogger.info( - 'Firebase already initialized or initialization failed: $e'); + 'Firebase initializeApp() skipped or failed (continuing): $e'); } // Initialize services @@ -226,7 +227,7 @@ class PushFireSDKImpl with WidgetsBindingObserver { _deviceRegisteredController.add(_currentDevice!); PushFireLogger.info('Device auto-registration completed'); } catch (e) { - PushFireLogger.error('Device auto-registration failed', e); + PushFireLogger.warning('Device auto-registration failed', e); // Don't throw here - allow SDK to continue working } } @@ -249,7 +250,7 @@ class PushFireSDKImpl with WidgetsBindingObserver { PushFireLogger.info('Device updated with new FCM token'); } catch (e) { - PushFireLogger.error('Failed to update device with new FCM token', e); + PushFireLogger.warning('Failed to update device with new FCM token', e); } }); } @@ -301,7 +302,7 @@ class PushFireSDKImpl with WidgetsBindingObserver { PushFireLogger.info('Device updated after permission status change'); } } catch (e) { - PushFireLogger.error( + PushFireLogger.warning( 'Failed to check permission status on app resume', e); } finally { _isCheckingPermission = false; @@ -318,21 +319,16 @@ class PushFireSDKImpl with WidgetsBindingObserver { }) async { _ensureInitialized(); - try { - _currentSubscriber = await _subscriberService.loginSubscriber( - externalId: externalId, - name: name, - email: email, - phone: phone, - metadata: metadata, - ); + _currentSubscriber = await _subscriberService.loginSubscriber( + externalId: externalId, + name: name, + email: email, + phone: phone, + metadata: metadata, + ); - _subscriberLoggedInController.add(_currentSubscriber!); - return _currentSubscriber!; - } catch (e) { - PushFireLogger.error('Subscriber login failed', e); - rethrow; - } + _subscriberLoggedInController.add(_currentSubscriber!); + return _currentSubscriber!; } /// Update subscriber @@ -349,47 +345,37 @@ class PushFireSDKImpl with WidgetsBindingObserver { throw const PushFireSubscriberException('No subscriber logged in'); } - try { - // Always use current subscriber's externalId (backend doesn't allow updates) - await _subscriberService.updateSubscriber( - subscriberId: currentSubscriber!.id!, - externalId: currentSubscriber.externalId, - name: name, - email: email, - phone: phone, - metadata: metadata, - ); + // Always use current subscriber's externalId (backend doesn't allow updates) + await _subscriberService.updateSubscriber( + subscriberId: currentSubscriber!.id!, + externalId: currentSubscriber.externalId, + name: name, + email: email, + phone: phone, + metadata: metadata, + ); - // Update the local subscriber state (externalId remains unchanged) - _currentSubscriber = currentSubscriber.copyWith( - name: name, - email: email, - phone: phone, - metadata: metadata, - ); + // Update the local subscriber state (externalId remains unchanged) + _currentSubscriber = currentSubscriber.copyWith( + name: name, + email: email, + phone: phone, + metadata: metadata, + ); - // Store updated subscriber data - await _subscriberService.storeSubscriberData(_currentSubscriber!); + // Store updated subscriber data + await _subscriberService.storeSubscriberData(_currentSubscriber!); - return _currentSubscriber!; - } catch (e) { - PushFireLogger.error('Subscriber update failed', e); - rethrow; - } + return _currentSubscriber!; } /// Logout subscriber Future logoutSubscriber() async { _ensureInitialized(); - try { - await _subscriberService.logoutSubscriber(); - _currentSubscriber = null; - _subscriberLoggedOutController.add(null); - } catch (e) { - PushFireLogger.error('Subscriber logout failed', e); - rethrow; - } + await _subscriberService.logoutSubscriber(); + _currentSubscriber = null; + _subscriberLoggedOutController.add(null); } /// Add tag to current subscriber @@ -576,26 +562,21 @@ class PushFireSDKImpl with WidgetsBindingObserver { Future reset() async { _ensureInitialized(); - try { - PushFireLogger.info('Resetting SDK'); + PushFireLogger.info('Resetting SDK'); - // Logout subscriber if logged in - if (await isSubscriberLoggedIn()) { - await logoutSubscriber(); - } + // Logout subscriber if logged in + if (await isSubscriberLoggedIn()) { + await logoutSubscriber(); + } - // Clear device data - await _deviceService.clearDeviceData(); + // Clear device data + await _deviceService.clearDeviceData(); - // Reset current state - _currentDevice = null; - _currentSubscriber = null; + // Reset current state + _currentDevice = null; + _currentSubscriber = null; - PushFireLogger.info('SDK reset completed'); - } catch (e) { - PushFireLogger.error('SDK reset failed', e); - rethrow; - } + PushFireLogger.info('SDK reset completed'); } /// Dispose SDK resources From e308302930ff676073ec06eaa3ad2870162ec21f Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 12:50:53 +0200 Subject: [PATCH 07/12] =?UTF-8?q?chore:=20release=200.1.12=20=E2=80=94=20b?= =?UTF-8?q?randed=20base=20URL=20and=20clearer=20error=20logging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 +++++++++++++ pubspec.yaml | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf45019..9a10b04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.1.12] + +### Changed +- **Default API base URL** is now `https://api.pushfire.app/functions/v1/` (was the raw Supabase functions URL). Functionally identical endpoint; keeps the backend project ref out of the SDK and logs. Override via `PushFireConfig.baseUrl` is unaffected. +- **Clearer API error logging**: failed requests now log a single `API error: -> HTTP code= msg=""` line instead of a misleading "Unexpected error" plus duplicate "X failed" lines across layers. + +### Fixed +- **Structured API exceptions are preserved**: a non-2xx response now throws a `PushFireApiException` whose `statusCode`, `code`, and `responseBody` are populated, instead of being re-wrapped into a generic exception with `statusCode == null`. +- **Timeouts are labeled**: request timeouts now throw `PushFireNetworkException` with a clear "timed out after Ns" message instead of "Unexpected error". + +### Internal +- `PushFireApiClient` accepts an injectable `http.Client` for testing; added API client error/timeout/network test coverage. + ## [0.1.11] ### Added diff --git a/pubspec.yaml b/pubspec.yaml index 64d7135..1a4d4b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A lightweight push notification tracking SDK for Firebase. homepage: https://github.com/FlywheelStudio/pushfire_sdk/blob/main/README.md repository: https://github.com/FlywheelStudio/pushfire_sdk -version: 0.1.11 +version: 0.1.12 environment: sdk: '>=3.3.1 <4.0.0' From aef70514766451d6195e845846fc47db1d0bf217 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 12:55:11 +0200 Subject: [PATCH 08/12] docs: explain logoutSubscriber's deliberate single-catch shape --- lib/src/services/subscriber_service.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/services/subscriber_service.dart b/lib/src/services/subscriber_service.dart index ac189ec..c72fd33 100644 --- a/lib/src/services/subscriber_service.dart +++ b/lib/src/services/subscriber_service.dart @@ -166,7 +166,10 @@ class SubscriberService { PushFireLogger.info('Subscriber logout completed'); } catch (e) { - // Clear local data even if API call fails + // Single catch (not `on PushFireException { rethrow; }` like the other + // methods) is deliberate: local data must be cleared on BOTH expected + // and unexpected failures. Clear first, then rethrow PushFireExceptions + // (already logged downstream) or wrap-and-log genuinely unexpected ones. await _clearSubscriberData(); if (e is PushFireException) { From 6fde00ba43b8e3819fa17169ca5f30a0cafa7835 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 13:09:35 +0200 Subject: [PATCH 09/12] chore: bump to 0.2.0; flag timeout exception change and PII logging in docs --- CHANGELOG.md | 6 ++++-- README.md | 2 ++ pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a10b04..b08363f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [0.1.12] +## [0.2.0] ### Changed - **Default API base URL** is now `https://api.pushfire.app/functions/v1/` (was the raw Supabase functions URL). Functionally identical endpoint; keeps the backend project ref out of the SDK and logs. Override via `PushFireConfig.baseUrl` is unaffected. @@ -8,7 +8,9 @@ ### Fixed - **Structured API exceptions are preserved**: a non-2xx response now throws a `PushFireApiException` whose `statusCode`, `code`, and `responseBody` are populated, instead of being re-wrapped into a generic exception with `statusCode == null`. -- **Timeouts are labeled**: request timeouts now throw `PushFireNetworkException` with a clear "timed out after Ns" message instead of "Unexpected error". + +### ⚠️ Potentially breaking +- **Request timeouts now throw `PushFireNetworkException`** with a clear "timed out after Ns" message. Previously a timeout surfaced as a generic `PushFireApiException` with an "Unexpected error" message. If you catch `PushFireApiException` specifically to handle timeouts, also catch `PushFireNetworkException` — or catch the shared base `PushFireException`. (`SocketException`/`HttpException` already mapped to `PushFireNetworkException`.) ### Internal - `PushFireApiClient` accepts an injectable `http.Client` for testing; added API client error/timeout/network test coverage. diff --git a/README.md b/README.md index 5b32282..3b28dcc 100644 --- a/README.md +++ b/README.md @@ -504,6 +504,8 @@ if (PushFireSDK.isInitialized) { | `authProvider` | AuthProvider | No | `AuthProvider.none` | Authentication provider for automatic subscriber management | | `requestNotificationPermission` | bool | No | `true` | Automatically request notification permissions during SDK initialization | +> **⚠️ Logging & privacy:** When `enableLogging` is `true`, the SDK logs full request and response bodies — including subscriber PII such as `email`, `phone`, `name`, and `metadata`. Logging is **off by default** and uses `dart:developer` (generally not emitted in release builds), but if you forward SDK logs anywhere (for example a crash reporter or log aggregator), do **not** do so in production builds with logging enabled. + ## Notification Permissions The PushFire SDK provides flexible notification permission handling to accommodate different app requirements and user experience strategies. diff --git a/pubspec.yaml b/pubspec.yaml index 1a4d4b2..619e049 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A lightweight push notification tracking SDK for Firebase. homepage: https://github.com/FlywheelStudio/pushfire_sdk/blob/main/README.md repository: https://github.com/FlywheelStudio/pushfire_sdk -version: 0.1.12 +version: 0.2.0 environment: sdk: '>=3.3.1 <4.0.0' From 1fc0cce7ec462a7e24b8a94091482269c733b741 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 13:13:43 +0200 Subject: [PATCH 10/12] test: strengthen API error logging coverage and assertions Extract formatApiError as a testable pure method in PushFireLogger, add logger unit tests, tighten existing API client test assertions (originalError, 5xx message/code, positive server-message check, relaxed non-JSON match), add timeout-seconds config test, and add subscriber logout tests verifying local data is cleared on both PushFireApiException and unexpected error paths. --- lib/src/utils/logger.dart | 23 ++++- test/api/pushfire_api_client_test.dart | 28 +++++- test/services/subscriber_service_test.dart | 109 +++++++++++++++++++++ test/utils/logger_test.dart | 23 +++++ 4 files changed, 173 insertions(+), 10 deletions(-) create mode 100644 test/services/subscriber_service_test.dart create mode 100644 test/utils/logger_test.dart diff --git a/lib/src/utils/logger.dart b/lib/src/utils/logger.dart index 78ec603..ed1a1bc 100644 --- a/lib/src/utils/logger.dart +++ b/lib/src/utils/logger.dart @@ -1,4 +1,5 @@ import 'dart:developer' as developer; +import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:logging/logging.dart'; /// Centralized logging utility for PushFire SDK @@ -84,6 +85,22 @@ class PushFireLogger { } } + /// Build the API error log line. Extracted as a pure function for testing. + @visibleForTesting + static String formatApiError( + String method, + String endpoint, + int statusCode, + String? code, + String message, + ) { + final buffer = + StringBuffer('API error: $method $endpoint -> HTTP $statusCode'); + if (code != null) buffer.write(' code=$code'); + buffer.write(' msg="$message"'); + return buffer.toString(); + } + /// Log an API error response as a single, clear error line static void logApiError( String method, @@ -93,11 +110,7 @@ class PushFireLogger { String message, ) { if (_enableLogging) { - final buffer = - StringBuffer('API error: $method $endpoint -> HTTP $statusCode'); - if (code != null) buffer.write(' code=$code'); - buffer.write(' msg="$message"'); - error(buffer.toString()); + error(formatApiError(method, endpoint, statusCode, code, message)); } } diff --git a/test/api/pushfire_api_client_test.dart b/test/api/pushfire_api_client_test.dart index 616e42f..45bc1e1 100644 --- a/test/api/pushfire_api_client_test.dart +++ b/test/api/pushfire_api_client_test.dart @@ -56,7 +56,7 @@ void main() { await expectLater( client.post('x', {}), throwsA(isA() - .having((e) => e.message, 'message', isNot(contains('Unexpected'))) + .having((e) => e.message, 'message', 'Nope') .having((e) => e.statusCode, 'statusCode', 403)), ); }); @@ -67,7 +67,9 @@ void main() { await expectLater( client.post('x', {}), throwsA(isA() - .having((e) => e.statusCode, 'statusCode', 500)), + .having((e) => e.statusCode, 'statusCode', 500) + .having((e) => e.message, 'message', 'boom') + .having((e) => e.code, 'code', isNull)), ); }); @@ -78,8 +80,7 @@ void main() { client.post('x', {}), throwsA(isA() .having((e) => e.statusCode, 'statusCode', 502) - .having((e) => e.message, 'message', - 'API request failed with status 502') + .having((e) => e.message, 'message', contains('502')) .having((e) => e.responseBody, 'responseBody', contains('502'))), ); }); @@ -102,7 +103,24 @@ void main() { await expectLater( client.post('x', {}), throwsA(isA() - .having((e) => e.message, 'message', contains('timed out'))), + .having((e) => e.message, 'message', contains('timed out')) + .having((e) => e.originalError, 'originalError', isA())), + ); + }); + + test('timeout message includes the configured seconds', () async { + final mock = MockClient((req) async => throw TimeoutException('slow')); + final client = PushFireApiClient( + const PushFireConfig( + apiKey: 'k', + baseUrl: 'https://api.pushfire.app/functions/v1/', + timeoutSeconds: 15), + httpClient: mock, + ); + await expectLater( + client.post('x', {}), + throwsA(isA() + .having((e) => e.message, 'message', contains('15s'))), ); }); }); diff --git a/test/services/subscriber_service_test.dart b/test/services/subscriber_service_test.dart new file mode 100644 index 0000000..42c3805 --- /dev/null +++ b/test/services/subscriber_service_test.dart @@ -0,0 +1,109 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:pushfire_sdk/src/api/pushfire_api_client.dart'; +import 'package:pushfire_sdk/src/config/pushfire_config.dart'; +import 'package:pushfire_sdk/src/exceptions/pushfire_exceptions.dart'; +import 'package:pushfire_sdk/src/models/subscriber.dart'; +import 'package:pushfire_sdk/src/services/device_service.dart'; +import 'package:pushfire_sdk/src/services/subscriber_service.dart'; + +/// Fake API client that allows configuring what post() throws or returns +class FakeApiClient extends PushFireApiClient { + Object? _postError; + Map _postResponse = {'success': true}; + + FakeApiClient() + : super(const PushFireConfig(apiKey: 'k', baseUrl: 'http://test/')); + + void throwOnPost(Object error) => _postError = error; + void returnOnPost(Map response) => _postResponse = response; + + @override + Future> post( + String endpoint, Map data) async { + if (_postError != null) throw _postError!; + return _postResponse; + } + + @override + Future> patch( + String endpoint, Map data) async { + return {'success': true}; + } +} + +/// Shared preferences keys (mirrors SubscriberService constants) +const _subscriberIdKey = 'pushfire_subscriber_id'; +const _subscriberDataKey = 'pushfire_subscriber_data'; +const _deviceIdKey = 'pushfire_device_id'; + +/// Seed SharedPreferences with a subscriber and a device id +void _seedPrefs() { + const subscriber = Subscriber( + id: 'sub-123', + deviceId: 'dev-abc', + externalId: 'user-1', + ); + SharedPreferences.setMockInitialValues({ + _subscriberIdKey: 'sub-123', + _subscriberDataKey: json.encode(subscriber.toJson()), + _deviceIdKey: 'dev-abc', + }); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('SubscriberService.logoutSubscriber - clears local data on error', () { + late FakeApiClient fakeApi; + late SubscriberService service; + + setUp(() { + fakeApi = FakeApiClient(); + final deviceService = DeviceService( + fakeApi, + const PushFireConfig(apiKey: 'k'), + ); + service = SubscriberService(fakeApi, deviceService); + }); + + test('clears local data when the API throws a PushFireApiException', () async { + _seedPrefs(); + + fakeApi.throwOnPost( + const PushFireApiException('Unauthorized', statusCode: 401), + ); + + await expectLater( + service.logoutSubscriber(), + throwsA(isA() + .having((e) => e.statusCode, 'statusCode', 401)), + ); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString(_subscriberIdKey), isNull, + reason: 'subscriber-id key should be cleared after logout error'); + expect(prefs.getString(_subscriberDataKey), isNull, + reason: 'subscriber-data key should be cleared after logout error'); + }); + + test('clears local data when the API throws an unexpected error', () async { + _seedPrefs(); + + fakeApi.throwOnPost(Exception('network blip')); + + await expectLater( + service.logoutSubscriber(), + throwsA(isA()), + ); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString(_subscriberIdKey), isNull, + reason: 'subscriber-id key should be cleared after unexpected error'); + expect(prefs.getString(_subscriberDataKey), isNull, + reason: 'subscriber-data key should be cleared after unexpected error'); + }); + }); +} diff --git a/test/utils/logger_test.dart b/test/utils/logger_test.dart new file mode 100644 index 0000000..f21eb0f --- /dev/null +++ b/test/utils/logger_test.dart @@ -0,0 +1,23 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:pushfire_sdk/src/utils/logger.dart'; + +void main() { + group('PushFireLogger.formatApiError', () { + test('includes code when present', () { + expect( + PushFireLogger.formatApiError( + 'PATCH', 'update-subscriber', 401, 'missing_auth', + 'Missing authorization header'), + 'API error: PATCH update-subscriber -> HTTP 401 ' + 'code=missing_auth msg="Missing authorization header"', + ); + }); + + test('omits code when null', () { + expect( + PushFireLogger.formatApiError('POST', 'x', 500, null, 'boom'), + 'API error: POST x -> HTTP 500 msg="boom"', + ); + }); + }); +} From 99538fbeb8cca80c08a7394b86598c919c5d6fcc Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 13:25:15 +0200 Subject: [PATCH 11/12] fix(impl): always clear session and emit logout event, even on logout API failure --- lib/src/pushfire_sdk_impl.dart | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/src/pushfire_sdk_impl.dart b/lib/src/pushfire_sdk_impl.dart index 618e5e1..774ff78 100644 --- a/lib/src/pushfire_sdk_impl.dart +++ b/lib/src/pushfire_sdk_impl.dart @@ -373,9 +373,15 @@ class PushFireSDKImpl with WidgetsBindingObserver { Future logoutSubscriber() async { _ensureInitialized(); - await _subscriberService.logoutSubscriber(); - _currentSubscriber = null; - _subscriberLoggedOutController.add(null); + try { + await _subscriberService.logoutSubscriber(); + } finally { + // SubscriberService clears persisted local data even when the logout + // API call fails, so always mirror that cleared session in memory and + // emit the logout event. Any error still propagates to the caller. + _currentSubscriber = null; + _subscriberLoggedOutController.add(null); + } } /// Add tag to current subscriber From fd0546b8c262c51f625d75a45fe9fa2bb3dd0253 Mon Sep 17 00:00:00 2001 From: Mohanned Binmiskeen Date: Mon, 22 Jun 2026 13:28:30 +0200 Subject: [PATCH 12/12] style: apply dart format to satisfy CI format check --- lib/src/api/pushfire_api_client.dart | 6 +++-- lib/src/pushfire_sdk_impl.dart | 22 ++++++------------- lib/src/services/device_service.dart | 10 +++++---- test/api/pushfire_api_client_test.dart | 21 ++++++++++++------ test/exceptions/pushfire_exceptions_test.dart | 3 ++- test/models/subscriber_test.dart | 19 +++++++++------- test/models/workflow_execution_test.dart | 21 ++++++++++++------ ..._service_notification_preference_test.dart | 12 ++++------ test/services/subscriber_service_test.dart | 6 +++-- test/utils/logger_test.dart | 5 ++--- 10 files changed, 68 insertions(+), 57 deletions(-) diff --git a/lib/src/api/pushfire_api_client.dart b/lib/src/api/pushfire_api_client.dart index 2ecacdc..55330fe 100644 --- a/lib/src/api/pushfire_api_client.dart +++ b/lib/src/api/pushfire_api_client.dart @@ -96,13 +96,15 @@ class PushFireApiClient { PushFireLogger.error(message, e); throw PushFireNetworkException(message, originalError: e); } on SocketException catch (e) { - PushFireLogger.error('Network error during $method $endpoint: ${e.message}', e); + PushFireLogger.error( + 'Network error during $method $endpoint: ${e.message}', e); throw PushFireNetworkException( 'Network error: ${e.message}', originalError: e, ); } on HttpException catch (e) { - PushFireLogger.error('HTTP error during $method $endpoint: ${e.message}', e); + PushFireLogger.error( + 'HTTP error during $method $endpoint: ${e.message}', e); throw PushFireNetworkException( 'HTTP error: ${e.message}', originalError: e, diff --git a/lib/src/pushfire_sdk_impl.dart b/lib/src/pushfire_sdk_impl.dart index 774ff78..930c71a 100644 --- a/lib/src/pushfire_sdk_impl.dart +++ b/lib/src/pushfire_sdk_impl.dart @@ -138,9 +138,9 @@ class PushFireSDKImpl with WidgetsBindingObserver { case AuthProvider.supabase: PushFireLogger.info('Listening for auth state changes'); // Listen for auth state changes - _supabaseAuthSubscription = - sp.Supabase.instance.client.auth.onAuthStateChange - .listen((data) async { + _supabaseAuthSubscription = sp + .Supabase.instance.client.auth.onAuthStateChange + .listen((data) async { final event = data.event; final session = data.session; // Handle auth state changes if needed @@ -151,8 +151,7 @@ class PushFireSDKImpl with WidgetsBindingObserver { final user = session.user; // Skip if already logged in as this user - final current = - await _subscriberService.getCurrentSubscriber(); + final current = await _subscriberService.getCurrentSubscriber(); if (current != null && current.externalId == user.id) { PushFireLogger.info( 'Subscriber already logged in as ${user.id}, skipping auto-login'); @@ -168,10 +167,7 @@ class PushFireSDKImpl with WidgetsBindingObserver { ? null : user.userMetadata?['full_name']; loginSubscriber( - externalId: user.id, - email: email, - phone: phone, - name: name); + externalId: user.id, email: email, phone: phone, name: name); } else if (event == sp.AuthChangeEvent.signedOut) { logoutSubscriber(); } @@ -185,8 +181,7 @@ class PushFireSDKImpl with WidgetsBindingObserver { PushFireLogger.info('User signed in: ${user.uid}'); // Skip if already logged in as this user - final current = - await _subscriberService.getCurrentSubscriber(); + final current = await _subscriberService.getCurrentSubscriber(); if (current != null && current.externalId == user.uid) { PushFireLogger.info( 'Subscriber already logged in as ${user.uid}, skipping auto-login'); @@ -202,10 +197,7 @@ class PushFireSDKImpl with WidgetsBindingObserver { ? null : user.phoneNumber; loginSubscriber( - externalId: user.uid, - email: email, - name: name, - phone: phone); + externalId: user.uid, email: email, name: name, phone: phone); } else { PushFireLogger.info('User signed out'); logoutSubscriber(); diff --git a/lib/src/services/device_service.dart b/lib/src/services/device_service.dart index 3355d82..40ce481 100644 --- a/lib/src/services/device_service.dart +++ b/lib/src/services/device_service.dart @@ -31,7 +31,9 @@ class DeviceService { @visibleForTesting final Future Function()? getFcmTokenOverride; - DeviceService(this._apiClient, this._config, { + DeviceService( + this._apiClient, + this._config, { this.isPushNotificationEnabledOverride, this.getDeviceInfoOverride, this.getFcmTokenOverride, @@ -474,9 +476,9 @@ class DeviceService { } on PushFireException { rethrow; } catch (e) { - PushFireLogger.error('Unexpected error setting notification preference', e); - throw PushFireDeviceException( - 'Failed to set notification preference: $e', + PushFireLogger.error( + 'Unexpected error setting notification preference', e); + throw PushFireDeviceException('Failed to set notification preference: $e', originalError: e); } } diff --git a/test/api/pushfire_api_client_test.dart b/test/api/pushfire_api_client_test.dart index 45bc1e1..b192816 100644 --- a/test/api/pushfire_api_client_test.dart +++ b/test/api/pushfire_api_client_test.dart @@ -16,7 +16,8 @@ void main() { group('PushFireApiClient success responses', () { test('2xx with JSON body returns decoded map', () async { - final mock = MockClient((req) async => http.Response('{"id":"sub_1"}', 200)); + final mock = + MockClient((req) async => http.Response('{"id":"sub_1"}', 200)); final client = PushFireApiClient(config, httpClient: mock); final result = await client.post('login-subscriber', {'data': {}}); expect(result['id'], 'sub_1'); @@ -50,7 +51,8 @@ void main() { }); test('error is NOT re-wrapped as "Unexpected error"', () async { - final mock = MockClient((req) async => http.Response('{"message":"Nope"}', 403)); + final mock = + MockClient((req) async => http.Response('{"message":"Nope"}', 403)); final client = PushFireApiClient(config, httpClient: mock); await expectLater( @@ -62,7 +64,8 @@ void main() { }); test('5xx sets the status code', () async { - final mock = MockClient((req) async => http.Response('{"message":"boom"}', 500)); + final mock = + MockClient((req) async => http.Response('{"message":"boom"}', 500)); final client = PushFireApiClient(config, httpClient: mock); await expectLater( client.post('x', {}), @@ -74,21 +77,24 @@ void main() { }); test('non-JSON error body falls back to a status message', () async { - final mock = MockClient((req) async => http.Response('502', 502)); + final mock = + MockClient((req) async => http.Response('502', 502)); final client = PushFireApiClient(config, httpClient: mock); await expectLater( client.post('x', {}), throwsA(isA() .having((e) => e.statusCode, 'statusCode', 502) .having((e) => e.message, 'message', contains('502')) - .having((e) => e.responseBody, 'responseBody', contains('502'))), + .having((e) => e.responseBody, 'responseBody', + contains('502'))), ); }); }); group('PushFireApiClient transport errors', () { test('SocketException becomes PushFireNetworkException', () async { - final mock = MockClient((req) async => throw const SocketException('no route')); + final mock = + MockClient((req) async => throw const SocketException('no route')); final client = PushFireApiClient(config, httpClient: mock); await expectLater( client.post('x', {}), @@ -104,7 +110,8 @@ void main() { client.post('x', {}), throwsA(isA() .having((e) => e.message, 'message', contains('timed out')) - .having((e) => e.originalError, 'originalError', isA())), + .having((e) => e.originalError, 'originalError', + isA())), ); }); diff --git a/test/exceptions/pushfire_exceptions_test.dart b/test/exceptions/pushfire_exceptions_test.dart index 2b92bf4..224bf87 100644 --- a/test/exceptions/pushfire_exceptions_test.dart +++ b/test/exceptions/pushfire_exceptions_test.dart @@ -41,7 +41,8 @@ void main() { group('toString', () { test('with message only', () { const exception = PushFireApiException('Something went wrong'); - expect(exception.toString(), 'PushFireApiException: Something went wrong'); + expect( + exception.toString(), 'PushFireApiException: Something went wrong'); }); test('with code only', () { diff --git a/test/models/subscriber_test.dart b/test/models/subscriber_test.dart index 67e22e0..232131a 100644 --- a/test/models/subscriber_test.dart +++ b/test/models/subscriber_test.dart @@ -82,8 +82,7 @@ void main() { ); expect( (nestedMetadataSubscriber.metadata!['preferences'] - as Map)['notifications'] - as Map, + as Map)['notifications'] as Map, {'email': true, 'sms': false}, ); }); @@ -158,8 +157,7 @@ void main() { final subscriber = Subscriber.fromJson(json); expect(subscriber.metadata, isNotNull); - final profile = - subscriber.metadata!['profile'] as Map; + final profile = subscriber.metadata!['profile'] as Map; expect(profile['bio'], 'Hello world'); expect(profile['links'], ['https://a.com', 'https://b.com']); expect(subscriber.metadata!['scores'], [100, 200, 300]); @@ -397,14 +395,20 @@ void main() { final a = Subscriber( externalId: 'ext-deep', metadata: { - 'nested': {'inner': 'value', 'list': [1, 2, 3]}, + 'nested': { + 'inner': 'value', + 'list': [1, 2, 3] + }, 'top': 'level', }, ); final b = Subscriber( externalId: 'ext-deep', metadata: { - 'nested': {'inner': 'value', 'list': [1, 2, 3]}, + 'nested': { + 'inner': 'value', + 'list': [1, 2, 3] + }, 'top': 'level', }, ); @@ -516,8 +520,7 @@ void main() { expect(a.hashCode, equals(b.hashCode)); }); - test( - 'metadata with different insertion order produces same hashCode', () { + test('metadata with different insertion order produces same hashCode', () { final metaA = {'x': 1, 'y': 2, 'z': 3}; final metaB = {}; diff --git a/test/models/workflow_execution_test.dart b/test/models/workflow_execution_test.dart index c8f1428..a4a6b95 100644 --- a/test/models/workflow_execution_test.dart +++ b/test/models/workflow_execution_test.dart @@ -433,7 +433,8 @@ void main() { expect(request.target, target); }); - test('scheduledFor is optional even for scheduled type at construction', () { + test('scheduledFor is optional even for scheduled type at construction', + () { // Construction does not validate; validate() does. final request = WorkflowExecutionRequest( workflowId: validUuid1, @@ -582,8 +583,8 @@ void main() { ); final json = request.toJson(); - final targetJson = - (json['data'] as Map)['target'] as Map; + final targetJson = (json['data'] as Map)['target'] + as Map; expect(targetJson['type'], 'Segments'); expect(targetJson['values'], [validUuid2, validUuid3]); @@ -850,7 +851,8 @@ void main() { () => request.validate(), throwsA( predicate( - (e) => e.message == + (e) => + e.message == 'All target values must be valid UUIDs: $badValue', ), ), @@ -859,7 +861,8 @@ void main() { }); group('validation priority order', () { - test('empty workflowId checked before scheduled without scheduledFor', () { + test('empty workflowId checked before scheduled without scheduledFor', + () { final request = WorkflowExecutionRequest( workflowId: '', type: WorkflowExecutionType.scheduled, @@ -876,7 +879,9 @@ void main() { ); }); - test('scheduled without scheduledFor checked before empty target values', () { + test( + 'scheduled without scheduledFor checked before empty target values', + () { final request = WorkflowExecutionRequest( workflowId: validUuid1, type: WorkflowExecutionType.scheduled, @@ -915,7 +920,9 @@ void main() { ); }); - test('invalid workflowId UUID checked before invalid target value UUIDs', () { + test( + 'invalid workflowId UUID checked before invalid target value UUIDs', + () { final request = WorkflowExecutionRequest( workflowId: 'not-uuid-format', type: WorkflowExecutionType.immediate, diff --git a/test/services/device_service_notification_preference_test.dart b/test/services/device_service_notification_preference_test.dart index 2bad202..2d790f7 100644 --- a/test/services/device_service_notification_preference_test.dart +++ b/test/services/device_service_notification_preference_test.dart @@ -212,10 +212,8 @@ void main() { final status = await service.getNotificationStatus(); - expect( - status, - const NotificationStatus( - isPermissionGranted: true, isEnabled: true)); + expect(status, + const NotificationStatus(isPermissionGranted: true, isEnabled: true)); }); test('returns OS granted + preference false', () async { @@ -250,10 +248,8 @@ void main() { final status = await service.getNotificationStatus(); - expect( - status, - const NotificationStatus( - isPermissionGranted: true, isEnabled: true)); + expect(status, + const NotificationStatus(isPermissionGranted: true, isEnabled: true)); }); test('works without device registration', () async { diff --git a/test/services/subscriber_service_test.dart b/test/services/subscriber_service_test.dart index 42c3805..3ec33e0 100644 --- a/test/services/subscriber_service_test.dart +++ b/test/services/subscriber_service_test.dart @@ -69,7 +69,8 @@ void main() { service = SubscriberService(fakeApi, deviceService); }); - test('clears local data when the API throws a PushFireApiException', () async { + test('clears local data when the API throws a PushFireApiException', + () async { _seedPrefs(); fakeApi.throwOnPost( @@ -103,7 +104,8 @@ void main() { expect(prefs.getString(_subscriberIdKey), isNull, reason: 'subscriber-id key should be cleared after unexpected error'); expect(prefs.getString(_subscriberDataKey), isNull, - reason: 'subscriber-data key should be cleared after unexpected error'); + reason: + 'subscriber-data key should be cleared after unexpected error'); }); }); } diff --git a/test/utils/logger_test.dart b/test/utils/logger_test.dart index f21eb0f..9136873 100644 --- a/test/utils/logger_test.dart +++ b/test/utils/logger_test.dart @@ -5,9 +5,8 @@ void main() { group('PushFireLogger.formatApiError', () { test('includes code when present', () { expect( - PushFireLogger.formatApiError( - 'PATCH', 'update-subscriber', 401, 'missing_auth', - 'Missing authorization header'), + PushFireLogger.formatApiError('PATCH', 'update-subscriber', 401, + 'missing_auth', 'Missing authorization header'), 'API error: PATCH update-subscriber -> HTTP 401 ' 'code=missing_auth msg="Missing authorization header"', );