234 lines
7.8 KiB
Dart
234 lines
7.8 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:motorcycle_dash/features/entitlements/widgets/pro_lock_badge.dart';
|
|
import 'package:motorcycle_dash/services/billing/product_catalog.dart';
|
|
import 'package:motorcycle_dash/services/entitlements/entitlement_providers.dart';
|
|
import 'package:motorcycle_dash/services/entitlements/entitlement_state.dart';
|
|
import 'package:motorcycle_dash/services/entitlements/feature_access_service.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('FeatureAccessService Matrix Tests', () {
|
|
test(
|
|
'Free tier allows basic cloud backup but denies all Pro-only features',
|
|
() {
|
|
final freeState = EntitlementState.free();
|
|
final service = FeatureAccessService(freeState);
|
|
|
|
// Cloud backup is permitted for free users
|
|
expect(service.canAccess(MotorSignalFeature.cloudBackup), isTrue);
|
|
|
|
final proOnlyFeatures = MotorSignalFeature.values.where(
|
|
(f) => f != MotorSignalFeature.cloudBackup,
|
|
);
|
|
for (final feature in proOnlyFeatures) {
|
|
expect(
|
|
service.canAccess(feature),
|
|
isFalse,
|
|
reason: 'Free tier must not access ${feature.name}',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
test('Active Pro Monthly grants all features', () {
|
|
final proActiveState = EntitlementState.proMonthlyActive(
|
|
productId: ProductCatalog.proMonthly,
|
|
expiresAt: DateTime.now().add(const Duration(days: 30)),
|
|
);
|
|
final service = FeatureAccessService(proActiveState);
|
|
|
|
for (final feature in MotorSignalFeature.values) {
|
|
expect(
|
|
service.canAccess(feature),
|
|
isTrue,
|
|
reason: 'Active Pro Monthly must access ${feature.name}',
|
|
);
|
|
}
|
|
});
|
|
|
|
test(
|
|
'Pro Monthly in Grace Period grants all features for rider continuity',
|
|
() {
|
|
final graceState = EntitlementState.normalized(
|
|
plan: MotorSignalPlan.proMonthly,
|
|
status: MotorSignalEntitlementStatus.proMonthlyGrace,
|
|
verificationStatus: EntitlementVerificationStatus.verified,
|
|
source: PurchaseSource.googlePlay,
|
|
productId: ProductCatalog.proMonthly,
|
|
expiresAt: DateTime.now().add(const Duration(days: 2)),
|
|
);
|
|
final service = FeatureAccessService(graceState);
|
|
|
|
for (final feature in MotorSignalFeature.values) {
|
|
expect(
|
|
service.canAccess(feature),
|
|
isTrue,
|
|
reason: 'Grace period must access ${feature.name}',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
test('Pro Monthly on Account Hold denies all Pro-only features', () {
|
|
final holdState = EntitlementState.normalized(
|
|
plan: MotorSignalPlan.proMonthly,
|
|
status: MotorSignalEntitlementStatus.proMonthlyAccountHold,
|
|
verificationStatus: EntitlementVerificationStatus.verified,
|
|
source: PurchaseSource.googlePlay,
|
|
productId: ProductCatalog.proMonthly,
|
|
);
|
|
final service = FeatureAccessService(holdState);
|
|
|
|
final proOnlyFeatures = MotorSignalFeature.values.where(
|
|
(f) => f != MotorSignalFeature.cloudBackup,
|
|
);
|
|
for (final feature in proOnlyFeatures) {
|
|
expect(
|
|
service.canAccess(feature),
|
|
isFalse,
|
|
reason: 'Account hold must deny ${feature.name}',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('Pro Monthly Paused denies all Pro-only features', () {
|
|
final pausedState = EntitlementState.normalized(
|
|
plan: MotorSignalPlan.proMonthly,
|
|
status: MotorSignalEntitlementStatus.proMonthlyPaused,
|
|
verificationStatus: EntitlementVerificationStatus.verified,
|
|
source: PurchaseSource.googlePlay,
|
|
productId: ProductCatalog.proMonthly,
|
|
);
|
|
final service = FeatureAccessService(pausedState);
|
|
|
|
final proOnlyFeatures = MotorSignalFeature.values.where(
|
|
(f) => f != MotorSignalFeature.cloudBackup,
|
|
);
|
|
for (final feature in proOnlyFeatures) {
|
|
expect(
|
|
service.canAccess(feature),
|
|
isFalse,
|
|
reason: 'Paused subscription must deny ${feature.name}',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('Pro Monthly Expired denies all Pro-only features', () {
|
|
final expiredState = EntitlementState.normalized(
|
|
plan: MotorSignalPlan.proMonthly,
|
|
status: MotorSignalEntitlementStatus.proMonthlyExpired,
|
|
verificationStatus: EntitlementVerificationStatus.expired,
|
|
source: PurchaseSource.googlePlay,
|
|
productId: ProductCatalog.proMonthly,
|
|
);
|
|
final service = FeatureAccessService(expiredState);
|
|
|
|
final proOnlyFeatures = MotorSignalFeature.values.where(
|
|
(f) => f != MotorSignalFeature.cloudBackup,
|
|
);
|
|
for (final feature in proOnlyFeatures) {
|
|
expect(
|
|
service.canAccess(feature),
|
|
isFalse,
|
|
reason: 'Expired subscription must deny ${feature.name}',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('Pro Lifetime grants all features permanently', () {
|
|
final lifetimeState = EntitlementState.proLifetime(
|
|
productId: ProductCatalog.proLifetime,
|
|
);
|
|
final service = FeatureAccessService(lifetimeState);
|
|
|
|
for (final feature in MotorSignalFeature.values) {
|
|
expect(
|
|
service.canAccess(feature),
|
|
isTrue,
|
|
reason: 'Pro Lifetime must access ${feature.name}',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('UI Lock & Badge Widgets Tests', () {
|
|
testWidgets('ProBadge renders with PRO label and styling', (tester) async {
|
|
await tester.pumpWidget(
|
|
const MaterialApp(home: Scaffold(body: ProBadge())),
|
|
);
|
|
|
|
expect(find.text('PRO'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets(
|
|
'ProLockedRow renders when user is Free and hides when user is Pro',
|
|
(tester) async {
|
|
// 1. When user is Free, row is visible
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
entitlementProvider.overrideWithValue(
|
|
const EntitlementState.free(),
|
|
),
|
|
],
|
|
child: const MaterialApp(
|
|
home: Scaffold(
|
|
body: ProLockedRow(
|
|
feature: MotorSignalFeature.csvExport,
|
|
title: Text('CSV Export'),
|
|
subtitle: Text('Export telemetry data as spreadsheet'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('CSV Export'), findsOneWidget);
|
|
expect(
|
|
find.text('Export telemetry data as spreadsheet'),
|
|
findsOneWidget,
|
|
);
|
|
expect(find.text('PRO'), findsOneWidget);
|
|
expect(find.byIcon(Icons.lock_outline), findsOneWidget);
|
|
|
|
// 2. When user is Pro, row hides (SizedBox.shrink)
|
|
final proState = EntitlementState.proLifetime(
|
|
productId: ProductCatalog.proLifetime,
|
|
);
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [entitlementProvider.overrideWithValue(proState)],
|
|
child: const MaterialApp(
|
|
home: Scaffold(
|
|
body: ProLockedRow(
|
|
feature: MotorSignalFeature.csvExport,
|
|
title: Text('CSV Export'),
|
|
subtitle: Text('Export telemetry data as spreadsheet'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('CSV Export'), findsNothing);
|
|
expect(find.text('PRO'), findsNothing);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'FeatureBenefitInfo provides title and description for all features',
|
|
() {
|
|
for (final feature in MotorSignalFeature.values) {
|
|
final info = FeatureBenefitInfo.forFeature(feature);
|
|
expect(info.title, isNotEmpty);
|
|
expect(info.description, isNotEmpty);
|
|
expect(info.icon, isNotNull);
|
|
}
|
|
},
|
|
);
|
|
});
|
|
}
|