111 lines
4.2 KiB
Dart
111 lines
4.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
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:motorcycle_dash/services/sync/r2_upload_service.dart';
|
|
import 'package:motorcycle_dash/services/sync/sync_models.dart';
|
|
import 'package:motorcycle_dash/services/sync/sync_outbox.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('Sync outbox hardening', () {
|
|
test('interrupted uploading item is requeued after restart', () async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'motorsignal_sync_outbox_manifest_v1': jsonEncode([
|
|
SyncItem(
|
|
localRideId: 42,
|
|
providerId: 'firebase',
|
|
status: SyncStatus.uploading,
|
|
createdAt: DateTime(2026, 9, 1),
|
|
).toJson(),
|
|
]),
|
|
});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final outbox = SyncOutbox(prefs);
|
|
expect(outbox.getItem(42, 'firebase')?.status, SyncStatus.queued);
|
|
});
|
|
|
|
test('duplicate enqueue remains one logical provider/ride item', () async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final outbox = SyncOutbox(prefs);
|
|
await Future.wait([
|
|
outbox.enqueue(7, 'firebase'),
|
|
outbox.enqueue(7, 'firebase'),
|
|
]);
|
|
expect(outbox.count, 1);
|
|
expect(outbox.getItem(7, 'firebase')?.idempotencyKey, 'firebase:7');
|
|
});
|
|
|
|
test('malformed persisted identity is ignored without killing the queue', () async {
|
|
SharedPreferences.setMockInitialValues({
|
|
'motorsignal_sync_outbox_manifest_v1': jsonEncode([
|
|
{'localRideId': -1, 'providerId': '', 'status': 'queued', 'createdAt': 'bad'},
|
|
SyncItem(localRideId: 8, providerId: 'firebase', createdAt: DateTime(2026, 9, 1)).toJson(),
|
|
]),
|
|
});
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final outbox = SyncOutbox(prefs);
|
|
expect(outbox.count, 1);
|
|
expect(outbox.getItem(8, 'firebase'), isNotNull);
|
|
});
|
|
});
|
|
|
|
group('R2 gateway hardening', () {
|
|
const validHash = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
|
|
|
test('valid response requires a user-scoped object key', () async {
|
|
final client = MockClient((request) async => http.Response(
|
|
jsonEncode({'objectKey': 'users/u1/rides/ride_1_20260901_120000/telemetry.zip', 'size': 4}),
|
|
200,
|
|
));
|
|
final service = R2UploadService(client: client, workerBaseUrl: 'https://worker.test');
|
|
final result = await service.uploadTelemetryBundle(
|
|
idToken: 'token',
|
|
remoteRideId: 'ride_1_20260901_120000',
|
|
zipBytes: Uint8List.fromList([1, 2, 3, 4]),
|
|
sha256Hash: validHash,
|
|
appVersion: '1.0.2',
|
|
);
|
|
expect(result.success, isTrue);
|
|
});
|
|
|
|
test('invalid identifiers are rejected before network I/O', () async {
|
|
var requests = 0;
|
|
final client = MockClient((request) async {
|
|
requests++;
|
|
return http.Response('{}', 200);
|
|
});
|
|
final service = R2UploadService(client: client, workerBaseUrl: 'https://worker.test');
|
|
final result = await service.uploadTelemetryBundle(
|
|
idToken: 'token',
|
|
remoteRideId: '../escape',
|
|
zipBytes: Uint8List.fromList([1]),
|
|
sha256Hash: validHash,
|
|
appVersion: '1.0.2',
|
|
);
|
|
expect(result.success, isFalse);
|
|
expect(result.isRetryable, isFalse);
|
|
expect(requests, 0);
|
|
});
|
|
|
|
test('rate limiting is retryable and delegated to SyncManager backoff', () async {
|
|
final client = MockClient((request) async => http.Response('rate limited', 429));
|
|
final service = R2UploadService(client: client, workerBaseUrl: 'https://worker.test');
|
|
final result = await service.uploadTelemetryBundle(
|
|
idToken: 'token',
|
|
remoteRideId: 'ride_1_20260901_120000',
|
|
zipBytes: Uint8List.fromList([1]),
|
|
sha256Hash: validHash,
|
|
appVersion: '1.0.2',
|
|
);
|
|
expect(result.success, isFalse);
|
|
expect(result.isRetryable, isTrue);
|
|
});
|
|
});
|
|
}
|