Add sync outbox and R2 hardening regression tests
This commit is contained in:
parent
7ed8af5b86
commit
4b8aa36bba
|
|
@ -0,0 +1,110 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue