205 lines
5.9 KiB
Dart
205 lines
5.9 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_service.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_state.dart';
|
|
import 'package:motorcycle_dash/services/auth/auth_user.dart';
|
|
import 'package:motorcycle_dash/services/cloud/account_cloud_cleanup_service.dart';
|
|
|
|
class _Auth implements AuthService {
|
|
@override Future<void> ensureBackendIdentity() async {}
|
|
|
|
final _states = StreamController<AuthState>.broadcast();
|
|
String? token = 'test-token';
|
|
AuthUser? user = const AuthUser(
|
|
uid: 'cleanup-test',
|
|
email: 'cleanup@test.com',
|
|
providerId: 'password',
|
|
emailVerified: true,
|
|
);
|
|
|
|
@override
|
|
Stream<AuthState> get authStateChanges => _states.stream;
|
|
@override
|
|
AuthState get currentState => user == null
|
|
? const AuthState.unauthenticated(isFirebaseConfigured: true)
|
|
: AuthState.authenticated(user!, isFirebaseConfigured: true);
|
|
@override
|
|
AuthUser? get currentUser => user;
|
|
@override
|
|
bool get isConfigured => true;
|
|
@override
|
|
bool get isEmailVerified => true;
|
|
@override
|
|
Future<void> reauthenticate({String? password}) async {}
|
|
@override
|
|
Future<void> deleteCurrentUser() async {}
|
|
@override
|
|
Future<String?> getIdToken([bool forceRefresh = false]) async => token;
|
|
@override
|
|
Future<void> signInWithEmailAndPassword(String email, String password) async {}
|
|
@override
|
|
Future<void> createUserWithEmailAndPassword(String email, String password, {String? displayName}) async {}
|
|
@override
|
|
Future<void> signInWithGoogle() async {}
|
|
@override
|
|
Future<void> signInAnonymously() async {}
|
|
@override
|
|
Future<void> sendPasswordResetEmail(String email) async {}
|
|
@override
|
|
Future<void> sendEmailVerification() async {}
|
|
@override
|
|
Future<void> reloadCurrentUser() async {}
|
|
@override
|
|
Future<void> signOut() async {}
|
|
@override
|
|
void dispose() => _states.close();
|
|
}
|
|
|
|
AccountCloudCleanupService _service(_Auth auth, int status, String body) {
|
|
return AccountCloudCleanupService(
|
|
auth: auth,
|
|
client: MockClient((request) async {
|
|
expect(request.method, 'DELETE');
|
|
expect(request.url.path, '/account/cloud-data');
|
|
expect(request.headers['authorization'], 'Bearer test-token');
|
|
return http.Response(body, status);
|
|
}),
|
|
workerUrl: 'https://cleanup.test',
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
test('accepts a valid Worker 200 response before Firestore cleanup', () async {
|
|
final auth = _Auth();
|
|
addTearDown(auth.dispose);
|
|
|
|
await expectLater(
|
|
_service(auth, 200, '{"success":true,"deletedObjects":0}').deleteCloudData(),
|
|
throwsA(isA<StateError>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
'MotorCloud database is unavailable.',
|
|
)),
|
|
);
|
|
});
|
|
|
|
test('accepts an idempotent empty-prefix 204 response', () async {
|
|
final auth = _Auth();
|
|
addTearDown(auth.dispose);
|
|
|
|
await expectLater(
|
|
_service(auth, 204, '').deleteCloudData(),
|
|
throwsA(isA<StateError>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
'MotorCloud database is unavailable.',
|
|
)),
|
|
);
|
|
});
|
|
|
|
test('rejects a malformed Worker success response', () async {
|
|
final auth = _Auth();
|
|
addTearDown(auth.dispose);
|
|
|
|
await expectLater(
|
|
_service(auth, 200, '{"deleted":0}').deleteCloudData(),
|
|
throwsA(isA<StateError>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
'MotorCloud cleanup returned an invalid response.',
|
|
)),
|
|
);
|
|
});
|
|
|
|
test('rejects an empty Worker success response', () async {
|
|
final auth = _Auth();
|
|
addTearDown(auth.dispose);
|
|
|
|
await expectLater(
|
|
_service(auth, 200, '').deleteCloudData(),
|
|
throwsA(isA<StateError>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
'MotorCloud cleanup returned an invalid response.',
|
|
)),
|
|
);
|
|
});
|
|
|
|
for (final status in [401, 403, 500]) {
|
|
test('surfaces Worker HTTP $status as controlled cleanup failure', () async {
|
|
final auth = _Auth();
|
|
addTearDown(auth.dispose);
|
|
|
|
await expectLater(
|
|
_service(auth, status, '{"error":"failure"}').deleteCloudData(),
|
|
throwsA(isA<StateError>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
'MotorCloud cleanup was not completed ($status).',
|
|
)),
|
|
);
|
|
});
|
|
}
|
|
|
|
test('rejects an empty token before making a Worker request', () async {
|
|
final auth = _Auth()..token = null;
|
|
addTearDown(auth.dispose);
|
|
|
|
await expectLater(
|
|
_service(auth, 200, '').deleteCloudData(),
|
|
throwsA(isA<StateError>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
'A valid account session is required.',
|
|
)),
|
|
);
|
|
});
|
|
|
|
test('rejects an anonymous account before making a Worker request', () async {
|
|
final auth = _Auth()
|
|
..user = const AuthUser(uid: 'guest', isAnonymous: true);
|
|
addTearDown(auth.dispose);
|
|
var requests = 0;
|
|
final service = AccountCloudCleanupService(
|
|
auth: auth,
|
|
client: MockClient((request) async {
|
|
requests++;
|
|
return http.Response('', 500);
|
|
}),
|
|
workerUrl: 'https://cleanup.test',
|
|
);
|
|
|
|
await expectLater(
|
|
service.deleteCloudData(),
|
|
throwsA(isA<StateError>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
'A permanent account is required.',
|
|
)),
|
|
);
|
|
expect(requests, 0);
|
|
});
|
|
|
|
test('rejects invalid Worker configuration before making a request', () async {
|
|
final auth = _Auth();
|
|
addTearDown(auth.dispose);
|
|
final service = AccountCloudCleanupService(
|
|
auth: auth,
|
|
client: MockClient((request) async => http.Response('', 200)),
|
|
workerUrl: '',
|
|
);
|
|
|
|
await expectLater(
|
|
service.deleteCloudData(),
|
|
throwsA(isA<StateError>().having(
|
|
(error) => error.message,
|
|
'message',
|
|
'MotorCloud cleanup endpoint is unavailable.',
|
|
)),
|
|
);
|
|
});
|
|
}
|