MotorSignal-App/test/club_membership_controller_...

283 lines
11 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:motorcycle_dash/features/social/clubs/club_membership_controller.dart';
import 'package:motorcycle_dash/services/auth/auth_providers.dart';
import 'package:motorcycle_dash/services/auth/auth_user.dart';
import 'package:motorcycle_dash/services/clubs/club_providers.dart';
import 'package:motorcycle_dash/services/clubs/club_service.dart';
class FakeClubServiceForController extends ClubService {
String? lastAction;
String? lastClubId;
String? lastCallerUid;
bool? lastAccept;
bool shouldThrow = false;
String? throwCode;
String? throwMessage;
Future<void> Function()? onJoinAsync;
@override
Future<void> joinOpenClub(String clubId, {String? callerUid}) async {
if (onJoinAsync != null) {
await onJoinAsync!();
}
if (shouldThrow) {
throw ClubOperationException(throwMessage ?? 'Error', code: throwCode);
}
lastAction = 'joinOpen';
lastClubId = clubId;
lastCallerUid = callerUid;
}
@override
Future<void> requestToJoinClub(String clubId, {String? callerUid}) async {
if (shouldThrow) {
throw ClubOperationException(throwMessage ?? 'Error', code: throwCode);
}
lastAction = 'requestToJoin';
lastClubId = clubId;
lastCallerUid = callerUid;
}
@override
Future<void> withdrawJoinRequest(String clubId, {String? callerUid}) async {
if (shouldThrow) {
throw ClubOperationException(throwMessage ?? 'Error', code: throwCode);
}
lastAction = 'withdrawRequest';
lastClubId = clubId;
lastCallerUid = callerUid;
}
@override
Future<void> respondToInvitation(String clubId, {required bool accept, String? callerUid}) async {
if (shouldThrow) {
throw ClubOperationException(throwMessage ?? 'Error', code: throwCode);
}
lastAction = 'respondInvite';
lastClubId = clubId;
lastAccept = accept;
lastCallerUid = callerUid;
}
@override
Future<void> leaveClub(String clubId, {String? callerUid}) async {
if (shouldThrow) {
throw ClubOperationException(throwMessage ?? 'Error', code: throwCode);
}
lastAction = 'leaveClub';
lastClubId = clubId;
lastCallerUid = callerUid;
}
}
void main() {
group('ClubMembershipController Tests', () {
late FakeClubServiceForController fakeService;
late ProviderContainer container;
setUp(() {
fakeService = FakeClubServiceForController();
container = ProviderContainer(
overrides: [
clubServiceProvider.overrideWithValue(fakeService),
currentUserProvider.overrideWith(
(ref) => const AuthUser(uid: 'user_rider_1', isAnonymous: false),
),
],
);
});
tearDown(() {
container.dispose();
});
test('joinOpenClub executes and transitions state to success', () async {
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.joinOpenClub('club_1');
expect(success, isTrue);
expect(fakeService.lastAction, 'joinOpen');
expect(fakeService.lastClubId, 'club_1');
expect(fakeService.lastCallerUid, 'user_rider_1');
final state = container.read(clubMembershipControllerProvider('club_1'));
expect(state.isSubmitting, isFalse);
expect(state.success, 'Successfully joined club!');
expect(state.error, isNull);
});
test('requestToJoinClub executes and transitions state to success', () async {
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.requestToJoinClub('club_1');
expect(success, isTrue);
expect(fakeService.lastAction, 'requestToJoin');
expect(fakeService.lastClubId, 'club_1');
final state = container.read(clubMembershipControllerProvider('club_1'));
expect(state.isSubmitting, isFalse);
expect(state.success, 'Join request submitted to club leadership.');
expect(state.error, isNull);
});
test('withdrawJoinRequest executes and transitions state to success', () async {
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.withdrawJoinRequest('club_1');
expect(success, isTrue);
expect(fakeService.lastAction, 'withdrawRequest');
final state = container.read(clubMembershipControllerProvider('club_1'));
expect(state.isSubmitting, isFalse);
expect(state.success, 'Join request withdrawn.');
});
test('acceptInvitation executes and transitions state to success', () async {
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.acceptInvitation('club_1');
expect(success, isTrue);
expect(fakeService.lastAction, 'respondInvite');
expect(fakeService.lastAccept, isTrue);
final state = container.read(clubMembershipControllerProvider('club_1'));
expect(state.success, contains('Welcome to the club'));
});
test('declineInvitation executes and transitions state to success', () async {
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.declineInvitation('club_1');
expect(success, isTrue);
expect(fakeService.lastAction, 'respondInvite');
expect(fakeService.lastAccept, isFalse);
final state = container.read(clubMembershipControllerProvider('club_1'));
expect(state.success, 'Invitation declined.');
});
test('leaveClub executes and transitions state to success', () async {
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.leaveClub('club_1');
expect(success, isTrue);
expect(fakeService.lastAction, 'leaveClub');
final state = container.read(clubMembershipControllerProvider('club_1'));
expect(state.success, 'You have left the club.');
});
test('fails gracefully when user is unauthenticated', () async {
final unauthContainer = ProviderContainer(
overrides: [
clubServiceProvider.overrideWithValue(fakeService),
currentUserProvider.overrideWith((ref) => null),
],
);
final controller = unauthContainer.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.joinOpenClub('club_1');
expect(success, isFalse);
final state = unauthContainer.read(clubMembershipControllerProvider('club_1'));
expect(state.error, contains('must be signed in'));
unauthContainer.dispose();
});
test('maps banned error code to friendly generic message without leaking mod info', () async {
fakeService.shouldThrow = true;
fakeService.throwCode = 'banned';
fakeService.throwMessage = 'User is in bans subcollection.';
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.joinOpenClub('club_1');
expect(success, isFalse);
final state = container.read(clubMembershipControllerProvider('club_1'));
expect(state.error, 'Membership is unavailable for this club.');
});
test('maps owner-cannot-leave error code properly', () async {
fakeService.shouldThrow = true;
fakeService.throwCode = 'owner-cannot-leave';
fakeService.throwMessage = 'Owner cannot leave.';
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success = await controller.leaveClub('club_1');
expect(success, isFalse);
final state = container.read(clubMembershipControllerProvider('club_1'));
expect(state.error, contains('transferring ownership first'));
});
test('maps policy-mismatch, not-found, and unavailable error codes', () async {
fakeService.shouldThrow = true;
fakeService.throwCode = 'policy-mismatch';
fakeService.throwMessage = 'Open join not allowed.';
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final success1 = await controller.joinOpenClub('club_1');
expect(success1, isFalse);
expect(container.read(clubMembershipControllerProvider('club_1')).error,
'This club does not allow open joining. Please submit a request.');
fakeService.throwCode = 'not-found';
fakeService.throwMessage = 'Not found';
final success2 = await controller.joinOpenClub('club_1');
expect(success2, isFalse);
expect(container.read(clubMembershipControllerProvider('club_1')).error,
'Club or membership record was not found.');
fakeService.throwCode = 'unavailable';
fakeService.throwMessage = 'Firestore down';
final success3 = await controller.joinOpenClub('club_1');
expect(success3, isFalse);
expect(container.read(clubMembershipControllerProvider('club_1')).error,
'Club services are temporarily unavailable. Please try again later.');
});
test('rejects concurrent duplicate mutation while in-flight', () async {
final completer = Completer<void>();
fakeService.onJoinAsync = () => completer.future;
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
final firstFuture = controller.joinOpenClub('club_1');
expect(container.read(clubMembershipControllerProvider('club_1')).isSubmitting, isTrue);
// Attempt second rapid mutation while first is still pending
final secondSuccess = await controller.joinOpenClub('club_1');
expect(secondSuccess, isFalse);
completer.complete();
final firstSuccess = await firstFuture;
expect(firstSuccess, isTrue);
expect(container.read(clubMembershipControllerProvider('club_1')).isSubmitting, isFalse);
});
test('isolates controller state per club family instance', () async {
final controllerA = container.read(clubMembershipControllerProvider('club_A').notifier);
final controllerB = container.read(clubMembershipControllerProvider('club_B').notifier);
await controllerA.joinOpenClub('club_A');
await controllerB.requestToJoinClub('club_B');
expect(container.read(clubMembershipControllerProvider('club_A')).success, 'Successfully joined club!');
expect(container.read(clubMembershipControllerProvider('club_B')).success, 'Join request submitted to club leadership.');
});
test('clearMessages resets state', () async {
final controller = container.read(clubMembershipControllerProvider('club_1').notifier);
await controller.joinOpenClub('club_1');
expect(container.read(clubMembershipControllerProvider('club_1')).success, isNotNull);
controller.clearMessages();
final cleared = container.read(clubMembershipControllerProvider('club_1'));
expect(cleared.success, isNull);
expect(cleared.error, isNull);
});
});
}