mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Co-authored-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co>
283 lines
8.6 KiB
Dart
283 lines
8.6 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:buzz/shared/diagnostics/diagnostics.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
late SharedPreferences preferences;
|
|
late _RecordingCrashReporter reporter;
|
|
late List<String> logs;
|
|
|
|
Future<DiagnosticsController> createController({
|
|
Map<String, Object> storedValues = const {},
|
|
String dsn = 'https://public@example.invalid/1',
|
|
}) async {
|
|
SharedPreferences.setMockInitialValues(storedValues);
|
|
preferences = await SharedPreferences.getInstance();
|
|
reporter = _RecordingCrashReporter();
|
|
logs = [];
|
|
return DiagnosticsController(
|
|
preferences: preferences,
|
|
config: SentryConfig(
|
|
dsn: dsn,
|
|
release: 'buzz@1.2.3',
|
|
dist: '42',
|
|
environment: 'production',
|
|
),
|
|
crashReporter: reporter,
|
|
log: logs.add,
|
|
);
|
|
}
|
|
|
|
test('missing preference defaults reporting on for first installs', () async {
|
|
final controller = await createController();
|
|
|
|
await controller.applyStartupConsent();
|
|
|
|
expect(controller.consentGranted, isTrue);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isNull);
|
|
expect(reporter.initializeCalls, 1);
|
|
expect(reporter.closeCalls, 0);
|
|
expect(
|
|
logs,
|
|
contains('Diagnostics enabled: Sentry initialized after user consent.'),
|
|
);
|
|
});
|
|
|
|
test('missing preference defaults reporting on after an upgrade', () async {
|
|
final controller = await createController(
|
|
storedValues: {'existing_preference_from_older_version': true},
|
|
);
|
|
|
|
await controller.applyStartupConsent();
|
|
|
|
expect(controller.consentGranted, isTrue);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isNull);
|
|
expect(reporter.initializeCalls, 1);
|
|
});
|
|
|
|
test('explicitly stored revocation remains off at startup', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: false},
|
|
);
|
|
|
|
await controller.applyStartupConsent();
|
|
|
|
expect(controller.consentGranted, isFalse);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
|
expect(reporter.initializeCalls, 0);
|
|
expect(reporter.closeCalls, 0);
|
|
expect(
|
|
logs,
|
|
contains(
|
|
'Diagnostics disabled: user consent is off; Sentry not initialized.',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('empty DSN prevents initialization with default-on reporting', () async {
|
|
final controller = await createController(dsn: ' ');
|
|
|
|
await controller.applyStartupConsent();
|
|
|
|
expect(controller.consentGranted, isTrue);
|
|
expect(controller.isConfigured, isFalse);
|
|
expect(reporter.initializeCalls, 0);
|
|
expect(
|
|
logs,
|
|
contains(
|
|
'Diagnostics disabled: SENTRY_DSN is empty; Sentry not initialized.',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('cannot opt in to an unconfigured build after revocation', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: false},
|
|
dsn: ' ',
|
|
);
|
|
|
|
await expectLater(controller.setConsent(true), throwsA(isA<StateError>()));
|
|
|
|
expect(controller.consentGranted, isFalse);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
|
expect(reporter.initializeCalls, 0);
|
|
expect(
|
|
logs,
|
|
contains(
|
|
'Diagnostics unchanged: SENTRY_DSN is empty; consent not enabled.',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('stored consent initializes once at startup', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: true},
|
|
);
|
|
|
|
await controller.applyStartupConsent();
|
|
await controller.applyStartupConsent();
|
|
|
|
expect(reporter.initializeCalls, 1);
|
|
expect(reporter.config?.release, 'buzz@1.2.3');
|
|
expect(reporter.config?.dist, '42');
|
|
expect(
|
|
logs,
|
|
contains('Diagnostics already enabled: Sentry initialization skipped.'),
|
|
);
|
|
});
|
|
|
|
test('explicit opt-in persists and initializes immediately', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: false},
|
|
);
|
|
|
|
await controller.setConsent(true);
|
|
|
|
expect(controller.consentGranted, isTrue);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isTrue);
|
|
expect(reporter.initializeCalls, 1);
|
|
expect(
|
|
logs,
|
|
contains('Diagnostics enabled: Sentry initialized after user consent.'),
|
|
);
|
|
});
|
|
|
|
test('disabling persists consent and closes immediately', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: true},
|
|
);
|
|
await controller.applyStartupConsent();
|
|
|
|
await controller.setConsent(false);
|
|
|
|
expect(controller.consentGranted, isFalse);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
|
expect(reporter.closeCalls, 1);
|
|
expect(
|
|
logs,
|
|
contains('Diagnostics disabled: user consent is off; Sentry closed.'),
|
|
);
|
|
});
|
|
|
|
test('can reinitialize after disabling', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: true},
|
|
);
|
|
await controller.applyStartupConsent();
|
|
|
|
await controller.setConsent(false);
|
|
await controller.setConsent(true);
|
|
|
|
expect(reporter.initializeCalls, 2);
|
|
expect(reporter.closeCalls, 1);
|
|
});
|
|
|
|
test('serializes concurrent consent changes', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: false},
|
|
);
|
|
reporter.initializeGate = Completer<void>();
|
|
|
|
final enable = controller.setConsent(true);
|
|
await reporter.initializeStarted.future;
|
|
final disable = controller.setConsent(false);
|
|
|
|
expect(reporter.closeCalls, 0);
|
|
reporter.initializeGate!.complete();
|
|
await Future.wait([enable, disable]);
|
|
|
|
expect(controller.consentGranted, isFalse);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
|
expect(reporter.initializeCalls, 1);
|
|
expect(reporter.closeCalls, 1);
|
|
});
|
|
|
|
test('initialization failure preserves explicit revocation', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: false},
|
|
);
|
|
reporter.initializeError = StateError('init failed');
|
|
|
|
await expectLater(controller.setConsent(true), throwsA(isA<StateError>()));
|
|
|
|
expect(controller.consentGranted, isFalse);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
|
expect(reporter.initializeCalls, 1);
|
|
});
|
|
|
|
test('close failure preserves persisted revocation', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: true},
|
|
);
|
|
await controller.applyStartupConsent();
|
|
reporter.closeError = StateError('close failed');
|
|
|
|
await expectLater(controller.setConsent(false), throwsA(isA<StateError>()));
|
|
|
|
expect(controller.consentGranted, isFalse);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
|
expect(reporter.closeCalls, 1);
|
|
});
|
|
|
|
test('continues accepting changes after a failed operation', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: false},
|
|
);
|
|
reporter.initializeError = StateError('init failed');
|
|
|
|
await expectLater(controller.setConsent(true), throwsA(isA<StateError>()));
|
|
reporter.initializeError = null;
|
|
await controller.setConsent(true);
|
|
|
|
expect(controller.consentGranted, isTrue);
|
|
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isTrue);
|
|
expect(reporter.initializeCalls, 2);
|
|
});
|
|
|
|
test('repeating explicit values is idempotent', () async {
|
|
final controller = await createController(
|
|
storedValues: {diagnosticsConsentPreferenceKey: false},
|
|
);
|
|
|
|
await controller.setConsent(false);
|
|
await controller.setConsent(true);
|
|
await controller.setConsent(true);
|
|
|
|
expect(reporter.initializeCalls, 1);
|
|
expect(reporter.closeCalls, 0);
|
|
});
|
|
}
|
|
|
|
class _RecordingCrashReporter implements CrashReporter {
|
|
int initializeCalls = 0;
|
|
int closeCalls = 0;
|
|
Object? initializeError;
|
|
Object? closeError;
|
|
Completer<void>? initializeGate;
|
|
final initializeStarted = Completer<void>();
|
|
SentryConfig? config;
|
|
|
|
@override
|
|
Future<void> initialize(SentryConfig config) async {
|
|
initializeCalls += 1;
|
|
this.config = config;
|
|
if (!initializeStarted.isCompleted) {
|
|
initializeStarted.complete();
|
|
}
|
|
await initializeGate?.future;
|
|
if (initializeError case final error?) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> close() async {
|
|
closeCalls += 1;
|
|
if (closeError case final error?) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|