MotorSignal-App/test/feature_gating_test.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);
}
},
);
});
}