diff --git a/CHANGELOG.md b/CHANGELOG.md index 45fa08f7b8..8d8d384ccd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 4.0.0-alpha.3 (Next) - Development. +- add loadContextsIntegration tests ## 4.0.0-alpha.2 diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index cdb891bf6e..5eededdf51 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -18,9 +18,15 @@ mixin SentryFlutter { OptionsConfiguration optionsConfiguration, Function callback, { PackageLoader packageLoader = _loadPackageInfo, + iOSPlatformChecker iOSPlatformChecker = _iOSPlatformChecker, }) async { await Sentry.init((options) async { - await _initDefaultValues(options, callback, packageLoader); + await _initDefaultValues( + options, + callback, + packageLoader, + iOSPlatformChecker, + ); await optionsConfiguration(options); }); @@ -30,6 +36,7 @@ mixin SentryFlutter { SentryOptions options, Function callback, PackageLoader packageLoader, + iOSPlatformChecker iOSPlatformChecker, ) async { // it is necessary to initialize Flutter method channels so that // our plugin can call into the native code. @@ -58,7 +65,7 @@ mixin SentryFlutter { // first step is to install the native integration and set default values, // so we are able to capture future errors. - _addDefaultIntegrations(options, callback); + _addDefaultIntegrations(options, callback, iOSPlatformChecker); await _setReleaseAndDist(options, packageLoader); @@ -103,6 +110,7 @@ mixin SentryFlutter { static void _addDefaultIntegrations( SentryOptions options, Function callback, + iOSPlatformChecker isIOS, ) { // the ordering here matters, as we'd like to first start the native integration // that allow us to send events to the network and then the Flutter integrations. @@ -121,8 +129,7 @@ mixin SentryFlutter { options.addIntegration(isolateErrorIntegration); } - // TODO: make it testable/mockable - if (Platform.isIOS) { + if (isIOS()) { options.addIntegration(loadContextsIntegration(options, _channel)); } // finally the runZonedGuarded, catch any errors in Dart code running @@ -145,7 +152,12 @@ mixin SentryFlutter { typedef PackageLoader = Future Function(); +typedef iOSPlatformChecker = bool Function(); + /// Package info loader. Future _loadPackageInfo() async { return await PackageInfo.fromPlatform(); } + +/// verify if the platform is iOS +bool _iOSPlatformChecker() => Platform.isIOS; diff --git a/flutter/test/default_integrations_test.dart b/flutter/test/default_integrations_test.dart index fd5c841b38..23c17da031 100644 --- a/flutter/test/default_integrations_test.dart +++ b/flutter/test/default_integrations_test.dart @@ -176,7 +176,7 @@ void main() { fixture.options.sdk.integrations.contains('nativeSdkIntegration')); }); - test('loadContextsIntegration adds integration on ios', () async { + test('loadContextsIntegration adds integration', () async { _channel.setMockMethodCallHandler((MethodCall methodCall) async {}); final integration = loadContextsIntegration(fixture.options, _channel); @@ -186,19 +186,6 @@ void main() { expect(true, fixture.options.sdk.integrations.contains('loadContextsIntegration')); }); - - test('loadContextsIntegration do not throw', () async { - _channel.setMockMethodCallHandler((MethodCall methodCall) async { - throw null; - }); - - final integration = loadContextsIntegration(fixture.options, _channel); - - await integration(fixture.hub, fixture.options); - - expect(true, - fixture.options.sdk.integrations.contains('loadContextsIntegration')); - }); } class Fixture { diff --git a/flutter/test/load_contexts_integrations_test.dart b/flutter/test/load_contexts_integrations_test.dart new file mode 100644 index 0000000000..8e3d5611ef --- /dev/null +++ b/flutter/test/load_contexts_integrations_test.dart @@ -0,0 +1,142 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; + +import 'mocks.dart'; + +void main() { + const MethodChannel _channel = MethodChannel('sentry_flutter'); + + TestWidgetsFlutterBinding.ensureInitialized(); + + bool called = false; + + setUp(() { + _channel.setMockMethodCallHandler((MethodCall methodCall) async { + called = true; + return { + 'integrations': ['NativeIntegration'], + 'package': {'sdk_name': 'native-package', 'version': '1.0'}, + 'contexts': { + 'device': {'name': 'Device1'}, + 'app': {'app_name': 'test-app'}, + 'os': {'name': 'os1'}, + 'gpu': {'name': 'gpu1'}, + 'browser': {'name': 'browser1'}, + 'runtime': {'name': 'RT1'}, + 'theme': 'material', + } + }; + }); + }); + + tearDown(() { + _channel.setMockMethodCallHandler(null); + }); + + test('should apply the loadContextsIntegration eventProcessor', () async { + final options = SentryOptions()..dsn = fakeDsn; + final hub = Hub(options); + + loadContextsIntegration(options, _channel)(hub, options); + + expect(options.eventProcessors.length, 1); + + final e = SentryEvent(); + final event = await options.eventProcessors.first(e, null); + + expect(called, true); + expect(event.contexts.device.name, 'Device1'); + expect(event.contexts.app.name, 'test-app'); + expect(event.contexts.operatingSystem.name, 'os1'); + expect(event.contexts.gpu.name, 'gpu1'); + expect(event.contexts.browser.name, 'browser1'); + expect( + event.contexts.runtimes.any((element) => element.name == 'RT1'), true); + expect(event.contexts['theme'], 'material'); + expect( + event.sdk.packages.any((element) => element.name == 'native-package'), + true, + ); + expect(event.sdk.integrations.contains('NativeIntegration'), true); + }); + + test( + 'should not override event contexts with the loadContextsIntegration infos', + () async { + final options = SentryOptions()..dsn = fakeDsn; + final hub = Hub(options); + + loadContextsIntegration(options, _channel)(hub, options); + + expect(options.eventProcessors.length, 1); + + final eventContexts = Contexts( + device: const Device(name: 'eDevice'), + app: const App(name: 'eApp'), + operatingSystem: const OperatingSystem(name: 'eOS'), + gpu: const Gpu(name: 'eGpu'), + browser: const Browser(name: 'eBrowser'), + runtimes: [const SentryRuntime(name: 'eRT')]) + ..['theme'] = 'cuppertino'; + final e = SentryEvent(contexts: eventContexts); + final event = await options.eventProcessors.first(e, null); + + expect(called, true); + expect(event.contexts.device.name, 'eDevice'); + expect(event.contexts.app.name, 'eApp'); + expect(event.contexts.operatingSystem.name, 'eOS'); + expect(event.contexts.gpu.name, 'eGpu'); + expect(event.contexts.browser.name, 'eBrowser'); + expect( + event.contexts.runtimes.any((element) => element.name == 'RT1'), true); + expect( + event.contexts.runtimes.any((element) => element.name == 'eRT'), true); + expect(event.contexts['theme'], 'cuppertino'); + }); + + test( + 'should merge event and loadContextsIntegration sdk packages and integration', + () async { + final options = SentryOptions()..dsn = fakeDsn; + final hub = Hub(options); + + loadContextsIntegration(options, _channel)(hub, options); + + final eventSdk = SdkVersion( + name: 'sdk1', + version: '1.0', + integrations: ['EventIntegration'], + packages: [const SentryPackage('event-package', '2.0')], + ); + final e = SentryEvent(sdk: eventSdk); + final event = await options.eventProcessors.first(e, null); + + expect( + event.sdk.packages.any((element) => element.name == 'native-package'), + true, + ); + expect( + event.sdk.packages.any((element) => element.name == 'event-package'), + true, + ); + expect(event.sdk.integrations.contains('NativeIntegration'), true); + expect(event.sdk.integrations.contains('EventIntegration'), true); + }, + ); + + test('should not throw on loadContextsIntegration exception', () async { + _channel.setMockMethodCallHandler((MethodCall methodCall) async { + throw null; + }); + final options = SentryOptions()..dsn = fakeDsn; + final hub = Hub(options); + + loadContextsIntegration(options, _channel)(hub, options); + + final e = SentryEvent(); + final event = await options.eventProcessors.first(e, null); + + expect(event, isNotNull); + }); +} diff --git a/flutter/test/mocks.dart b/flutter/test/mocks.dart index 0bb963738a..52e59d29ff 100644 --- a/flutter/test/mocks.dart +++ b/flutter/test/mocks.dart @@ -3,4 +3,6 @@ import 'package:sentry/sentry.dart'; class MockHub extends Mock implements Hub {} +class MockTransport extends Mock implements Transport {} + const fakeDsn = 'https://abc@def.ingest.sentry.io/1234567'; diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index 12a0fbcaa9..7a36d06865 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -1,9 +1,11 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; import 'package:package_info/package_info.dart'; import 'package:sentry/sentry.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'mocks.dart'; import 'sentry_flutter_util.dart'; void main() { @@ -22,11 +24,77 @@ void main() { test('Flutter init for mobile will run default configurations', () async { await SentryFlutter.init( - configurationTester, + getConfigurationTester(), callback, packageLoader: loadTestPackage, + iOSPlatformChecker: () => false, ); }); + + test('Flutter init for mobile will run default configurations on ios', + () async { + await SentryFlutter.init( + getConfigurationTester(isIOS: true), + callback, + packageLoader: loadTestPackage, + iOSPlatformChecker: () => true, + ); + }); + + group('platform based loadContextsIntegration', () { + final transport = MockTransport(); + + setUp(() { + _channel.setMockMethodCallHandler( + (MethodCall methodCall) async => {}, + ); + when(transport.send(any)) + .thenAnswer((realInvocation) => Future.value(SentryId.newId())); + }); + + tearDown(() { + _channel.setMockMethodCallHandler(null); + Sentry.close(); + }); + + test('should add loadContextsIntegration on ios', () async { + await SentryFlutter.init( + (options) => options + ..dsn = fakeDsn + ..transport = transport, + callback, + packageLoader: loadTestPackage, + iOSPlatformChecker: () => true, + ); + + await Sentry.captureMessage('a message'); + + final event = + verify(transport.send(captureAny)).captured.first as SentryEvent; + + expect(event.sdk.integrations.length, 5); + expect(event.sdk.integrations.contains('loadContextsIntegration'), true); + }); + + test('should not add loadContextsIntegration if not ios', () async { + await SentryFlutter.init( + (options) => options + ..dsn = fakeDsn + ..transport = transport, + callback, + packageLoader: loadTestPackage, + iOSPlatformChecker: () => false, + ); + + await Sentry.captureMessage('a message'); + + final event = + verify(transport.send(captureAny)).captured.first as SentryEvent; + + expect(event.sdk.integrations.length, 4); + expect(event.sdk.integrations.contains('loadContextsIntegration'), false); + }); + }); } void callback() {} diff --git a/flutter/test/sentry_flutter_util.dart b/flutter/test/sentry_flutter_util.dart index d9f9fe3c28..5dfdf6923e 100644 --- a/flutter/test/sentry_flutter_util.dart +++ b/flutter/test/sentry_flutter_util.dart @@ -6,36 +6,42 @@ import 'package:sentry/sentry.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/src/file_system_transport.dart'; import 'package:sentry_flutter/src/version.dart'; + import 'mocks.dart'; -FutureOr configurationTester( - SentryOptions options, { +FutureOr Function(SentryOptions) getConfigurationTester({ + bool isIOS = false, bool isWeb = false, -}) async { - options.dsn = fakeDsn; - - expect(kDebugMode, options.debug); - expect('debug', options.environment); - - expect(true, options.transport is FileSystemTransport); - - expect( - options.integrations - .where((element) => element == flutterErrorIntegration), - isNotEmpty); - - expect( - options.integrations - .where((element) => element == isolateErrorIntegration), - isNotEmpty); - - expect(4, options.integrations.length); - - expect(sdkName, options.sdk.name); - expect(sdkVersion, options.sdk.version); - expect('pub:sentry_flutter', options.sdk.packages.last.name); - expect(sdkVersion, options.sdk.packages.last.version); - - expect('packageName@version+buildNumber', options.release); - expect('buildNumber', options.dist); -} +}) => + (SentryOptions options) async { + options.dsn = fakeDsn; + + expect(kDebugMode, options.debug); + expect('debug', options.environment); + + expect(true, options.transport is FileSystemTransport); + + expect( + options.integrations + .where((element) => element == flutterErrorIntegration), + isNotEmpty); + + expect( + options.integrations + .where((element) => element == isolateErrorIntegration), + isNotEmpty); + + if (isIOS) { + expect(5, options.integrations.length); + } else { + expect(4, options.integrations.length); + } + + expect(sdkName, options.sdk.name); + expect(sdkVersion, options.sdk.version); + expect('pub:sentry_flutter', options.sdk.packages.last.name); + expect(sdkVersion, options.sdk.packages.last.version); + + expect('packageName@version+buildNumber', options.release); + expect('buildNumber', options.dist); + };