diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index b57ae00a..3cd894d1 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -94,7 +94,7 @@ jobs: flutter-version: ${{ env.flutter_version }} - name: "Install Tools" run: | - dart pub global activate pana + flutter pub global activate pana flutter pub global activate melos - name: "Bootstrap Workspace" run: melos bootstrap --verbose diff --git a/insights/lib/src/algolia_event_service.dart b/insights/lib/src/algolia_event_service.dart index a4a75478..858b2659 100644 --- a/insights/lib/src/algolia_event_service.dart +++ b/insights/lib/src/algolia_event_service.dart @@ -1,14 +1,32 @@ import 'package:algolia_client_insights/algolia_client_insights.dart'; import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; import 'event.dart'; import 'event_service.dart'; import 'lib_version.dart'; +/// Abstraction over the Insights client's push API to enable testing. +/// @nodoc - Internal use only, exposed for testing. +@internal +abstract class PushEvents { + Future pushEvents({required InsightsEvents insightsEvents}); +} + +class _InsightsClient implements PushEvents { + final InsightsClient _client; + _InsightsClient(this._client); + + @override + Future pushEvents({required InsightsEvents insightsEvents}) { + return _client.pushEvents(insightsEvents: insightsEvents); + } +} + /// EventService implementation using community client instance class AlgoliaEventService implements EventService { - /// Client instance - InsightsClient _client; + /// client instance + final PushEvents _client; /// Logger instance final Logger _log; @@ -33,21 +51,33 @@ class AlgoliaEventService implements EventService { ), ); - /// Creates [AlgoliaEventService] instance. - AlgoliaEventService.create(this._client) + /// Creates [AlgoliaEventService] instance from an InsightsClient. + AlgoliaEventService.create(InsightsClient client) + : this.withClient(_InsightsClient(client)); + + /// Creates [AlgoliaEventService] instance with a custom client. + /// @nodoc - Internal use only, exposed for testing. + @internal + AlgoliaEventService.withClient(this._client) : _log = Logger('Algolia/EventsService'); @override - void send(List events) => _client - .pushEvents( - insightsEvents: InsightsEvents( - events: - events.map((e) => e.toAlgoliaEvent()).where((e) => e != null)), - ) - .then( - (_) => _log.fine('Events upload: $events'), - onError: (exception) => _log.severe('Events upload error: $exception'), - ); + void send(List events) { + final validEvents = + events.map((e) => e.toAlgoliaEvent()).where((e) => e != null).toList(); + if (validEvents.isEmpty) { + return; + } + _client + .pushEvents( + insightsEvents: InsightsEvents(events: validEvents), + ) + .then( + (_) => _log.fine('Events upload: $events'), + onError: (exception) => + _log.severe('Events upload error: $exception'), + ); + } } extension AlgoliaEventConversion on Event { diff --git a/insights/test/algolia_event_service_test.dart b/insights/test/algolia_event_service_test.dart new file mode 100644 index 00000000..817be8e8 --- /dev/null +++ b/insights/test/algolia_event_service_test.dart @@ -0,0 +1,107 @@ +import 'package:test/test.dart'; +import 'package:algolia_client_insights/algolia_client_insights.dart'; +import 'package:algolia_insights/src/algolia_event_service.dart'; +import 'package:algolia_insights/src/event.dart'; + +class TestClient implements PushEvents { + int pushEventsCallCount = 0; + @override + Future pushEvents({required InsightsEvents insightsEvents}) async { + pushEventsCallCount++; + } +} + +void main() { + group('AlgoliaEventService', () { + test('does not call pushEvents when validEvents is empty', () async { + final testClient = TestClient(); + final service = AlgoliaEventService.withClient(testClient); + final events = []; + + service.send(events); + + // No events, so pushEvents should not be called + expect(testClient.pushEventsCallCount, equals(0)); + }); + + test('calls pushEvents when all events are valid', () async { + final testClient = TestClient(); + final service = AlgoliaEventService.withClient(testClient); + final events = [ + Event.clickHits( + 'test_event', + 'test_index', + 'test_user_token', + ['object1', 'object2'], + ), + Event.viewHits( + 'test_view_event', + 'test_index', + 'test_user_token', + ['object3', 'object4'], + ), + ]; + + service.send(events); + + // All events are valid, so pushEvents should be called once + expect(testClient.pushEventsCallCount, equals(1)); + }); + + test('calls pushEvents when some events are valid', () async { + final testClient = TestClient(); + final service = AlgoliaEventService.withClient(testClient); + final events = [ + Event.clickHits( + 'test_event', + 'test_index', + 'test_user_token', + ['object1', 'object2'], + ), + // This event has no objectIDs or filterValues, so it will be invalid + Event.clickHits( + 'invalid_event', + 'test_index', + 'test_user_token', + [], + ), + ]; + + service.send(events); + + // At least one event is valid, so pushEvents should be called once + expect(testClient.pushEventsCallCount, equals(1)); + }); + + test('does not call pushEvents when all events are invalid', () async { + final testClient = TestClient(); + final service = AlgoliaEventService.withClient(testClient); + final events = [ + // All these events have no objectIDs or filterValues, so they're invalid + Event.clickHits( + 'invalid_event_1', + 'test_index', + 'test_user_token', + [], + ), + Event.viewHits( + 'invalid_event_2', + 'test_index', + 'test_user_token', + [], + ), + Event.convertHits( + 'invalid_event_3', + 'test_index', + 'test_user_token', + [], + ), + ]; + + service.send(events); + + // All events are invalid, so pushEvents should not be called + expect(testClient.pushEventsCallCount, equals(0)); + }); + }); +}