215 lines
7.7 KiB
Dart
215 lines
7.7 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:drift/drift.dart' as drift;
|
|
import 'package:drift/native.dart';
|
|
import 'package:motorcycle_dash/core/app_info.dart';
|
|
import 'package:motorcycle_dash/core/version/app_identity.dart';
|
|
import 'package:motorcycle_dash/database/database.dart';
|
|
import 'package:motorcycle_dash/features/rides/ride_state_machine.dart';
|
|
import 'package:motorcycle_dash/models/recording_health_status.dart';
|
|
import 'package:motorcycle_dash/services/garage/motorcycle_image_service.dart';
|
|
import 'package:motorcycle_dash/services/github/github_auth_service.dart';
|
|
import 'package:motorcycle_dash/services/github/github_sync_service.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('MotorSignal v1.0.2+102 — Maintenance & Feature Verifications', () {
|
|
late AppDatabase db;
|
|
late SharedPreferences prefs;
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
prefs = await SharedPreferences.getInstance();
|
|
db = AppDatabase.forTesting(NativeDatabase.memory());
|
|
});
|
|
|
|
tearDown(() async {
|
|
await db.close();
|
|
});
|
|
|
|
test('1. Application Versioning & Identity Verification', () {
|
|
final identity = AppIdentity.current();
|
|
expect(identity.semanticVersion, equals('1.0.2'));
|
|
expect(identity.buildNumber, equals(102));
|
|
expect(AppInfo.fallbackVersion, equals('1.0.2+102'));
|
|
});
|
|
|
|
test('2. Consecutive Ride Session Performance Reset Verification', () {
|
|
// Simulate state machine with non-zero values from Ride #10
|
|
var session = RideSession(
|
|
dbId: 10,
|
|
state: RideState.finished,
|
|
healthStatus: RecordingHealthStatus.recordingComplete,
|
|
tripDistanceMiles: 1.59,
|
|
topSpeedMph: 57.98,
|
|
maxLeanLeft: 29.57,
|
|
maxLeanRight: 22.37,
|
|
maxAccelG: 0.45,
|
|
maxBrakingG: -0.72,
|
|
averageSpeedMph: 32.1,
|
|
);
|
|
|
|
expect(session.maxLeanLeft, equals(29.57));
|
|
expect(session.maxLeanRight, equals(22.37));
|
|
|
|
// Starting new ride (Ride #11) must reset all metrics to 0
|
|
final newSession = session.copyWith(
|
|
dbId: 11,
|
|
state: RideState.riding,
|
|
healthStatus: RecordingHealthStatus.recordingActive,
|
|
startTime: DateTime.now(),
|
|
tripDistanceMiles: 0,
|
|
topSpeedMph: 0,
|
|
maxLeanLeft: 0,
|
|
maxLeanRight: 0,
|
|
maxAccelG: 0,
|
|
maxBrakingG: 0,
|
|
averageSpeedMph: 0,
|
|
);
|
|
|
|
expect(newSession.dbId, equals(11));
|
|
expect(newSession.maxLeanLeft, equals(0.0));
|
|
expect(newSession.maxLeanRight, equals(0.0));
|
|
expect(newSession.maxAccelG, equals(0.0));
|
|
expect(newSession.maxBrakingG, equals(0.0));
|
|
expect(newSession.topSpeedMph, equals(0.0));
|
|
expect(newSession.tripDistanceMiles, equals(0.0));
|
|
});
|
|
|
|
test('3. Garage Motorcycle Photographic Reference Resolution', () {
|
|
final imageService = MotorcycleImageService();
|
|
|
|
// Exact 2001 Yamaha YZF-R6 lookup
|
|
final r6Ref = imageService.getReferenceImage(
|
|
make: 'Yamaha',
|
|
model: 'YZF-R6',
|
|
year: 2001,
|
|
);
|
|
expect(r6Ref.hasImage, isTrue);
|
|
expect(r6Ref.isPhoto, isTrue);
|
|
expect(r6Ref.assetPath, contains('yamaha_yzf_r6_2001.png'));
|
|
expect(r6Ref.caption, contains('2001 Yamaha YZF-R6'));
|
|
|
|
// Fallback for uncataloged motorcycle
|
|
final fallbackRef = imageService.getReferenceImage(
|
|
make: 'Custom',
|
|
model: 'Chopper',
|
|
year: 1995,
|
|
);
|
|
expect(fallbackRef.isPhoto, isFalse);
|
|
expect(fallbackRef.hasImage, isFalse);
|
|
expect(fallbackRef.attribution, contains('Schematic'));
|
|
});
|
|
|
|
test('4. GitHub Auth Service Token & Connection Test', () async {
|
|
final mockClient = MockClient((request) async {
|
|
if (request.url.path.contains('repos/bponders12-cell/ride_logs')) {
|
|
if (request.headers['Authorization'] == 'Bearer valid_token') {
|
|
return http.Response(jsonEncode({
|
|
'full_name': 'bponders12-cell/ride_logs',
|
|
'default_branch': 'main',
|
|
'permissions': {'push': true},
|
|
}), 200);
|
|
} else {
|
|
return http.Response(jsonEncode({'message': 'Bad credentials'}), 401);
|
|
}
|
|
}
|
|
return http.Response('Not Found', 404);
|
|
});
|
|
|
|
final auth = GitHubAuthService(prefs, mockClient);
|
|
|
|
// Save token
|
|
await auth.saveToken('valid_token');
|
|
expect(auth.getToken(), equals('valid_token'));
|
|
|
|
// Test valid connection
|
|
final resSuccess = await auth.testConnection();
|
|
expect(resSuccess['success'], isTrue);
|
|
expect(resSuccess['canPush'], isTrue);
|
|
|
|
// Test invalid token
|
|
await auth.saveToken('invalid_token');
|
|
final resFail = await auth.testConnection();
|
|
expect(resFail['success'], isFalse);
|
|
expect(resFail['message'], contains('401'));
|
|
|
|
// Clear token
|
|
await auth.clearToken();
|
|
expect(auth.getToken(), isNull);
|
|
});
|
|
|
|
test('5. GitHub Sync Service Offline-First & Upload Flow', () async {
|
|
// 1. Seed a test ride with telemetry and summary
|
|
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
|
|
startTime: DateTime(2026, 8, 30, 8, 0, 0),
|
|
endTime: drift.Value(DateTime(2026, 8, 30, 8, 30, 0)),
|
|
distanceMiles: const drift.Value(15.5),
|
|
topSpeedMph: const drift.Value(100.8),
|
|
maxLeanLeft: const drift.Value(22.4),
|
|
maxLeanRight: const drift.Value(22.6),
|
|
maxAccelG: const drift.Value(0.65),
|
|
maxBrakingG: const drift.Value(-1.05),
|
|
averageSpeed: const drift.Value(45.2),
|
|
));
|
|
|
|
await db.into(db.fieldTestSummaries).insert(FieldTestSummariesCompanion.insert(
|
|
rideId: rideId,
|
|
startTime: DateTime(2026, 8, 30, 8, 0, 0),
|
|
endTime: drift.Value(DateTime(2026, 8, 30, 8, 30, 0)),
|
|
totalSamples: const drift.Value(17237),
|
|
rideQualityScore: const drift.Value(100),
|
|
rideQualityRating: const drift.Value('EXCELLENT'),
|
|
));
|
|
|
|
final mockClient = MockClient((request) async {
|
|
if (request.method == 'GET') {
|
|
// File check before PUT
|
|
return http.Response(jsonEncode({'message': 'Not Found'}), 404);
|
|
} else if (request.method == 'PUT') {
|
|
return http.Response(jsonEncode({
|
|
'commit': {'sha': 'c0ffee1234567890'},
|
|
}), 201);
|
|
}
|
|
return http.Response('Bad request', 400);
|
|
});
|
|
|
|
final auth = GitHubAuthService(prefs, mockClient);
|
|
await auth.saveToken('test_pat');
|
|
|
|
final syncService = GitHubSyncService(db, auth, prefs, mockClient);
|
|
|
|
// Perform upload via the explicit GitHub PAT export path
|
|
final result = await syncService.uploadRideToGitHub(rideId);
|
|
expect(result.success, isTrue);
|
|
expect(result.commitSha, equals('c0ffee1234567890'));
|
|
});
|
|
|
|
test('6. GitHub Sync Service Graceful Failure & Local Data Preservation', () async {
|
|
// Seed a test ride
|
|
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
|
|
startTime: DateTime(2026, 8, 30, 8, 0, 0),
|
|
));
|
|
|
|
// Simulate network error
|
|
final mockClient = MockClient((request) async {
|
|
throw http.ClientException('Connection reset by peer');
|
|
});
|
|
|
|
final auth = GitHubAuthService(prefs, mockClient);
|
|
await auth.saveToken('test_pat');
|
|
|
|
final syncService = GitHubSyncService(db, auth, prefs, mockClient);
|
|
|
|
final result = await syncService.uploadRideToGitHub(rideId);
|
|
expect(result.success, isFalse);
|
|
expect(syncService.state, equals(GitHubUploadState.failed));
|
|
expect(result.message.toLowerCase(), contains('failed'));
|
|
});
|
|
});
|
|
}
|