import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/features/auth/auth_models.dart'; import 'package:patbond_flutter/features/community/community_models.dart'; import 'package:patbond_flutter/features/profile/profile_controller.dart'; import '../../helpers/auth_test_helpers.dart'; import '../../helpers/community_test_helpers.dart'; void main() { late FakeCommunityRepository community; setUp(() { community = FakeCommunityRepository(); }); ProfileController build(FakeAuthRepository auth) => ProfileController(authRepository: auth, communityRepository: community); test('四态:initial → loading → ready(资料 + 两块统计齐备)', () async { final gate = Completer(); final controller = build(FakeAuthRepository(meHandler: () => gate.future)); community.onGetMyCommunityStats = () async => CommunityStats.fromJson(sampleCommunityStatsJson()); community.onGetFollowStats = (_) async => FollowStats.fromJson(sampleFollowStatsJson()); expect(controller.phase, ProfileLoadPhase.initial); final pending = controller.refresh(); expect(controller.phase, ProfileLoadPhase.loading); gate.complete(buildProfile(nickname: '小柴')); await pending; expect(controller.phase, ProfileLoadPhase.ready); expect(controller.statsPhase, ProfileStatsPhase.ready); expect(controller.displayName, '小柴'); expect(controller.communityStats!.receivedLikeCount, 128); expect(controller.followStats!.followerCount, 24); }); test('/me 失败且无副本 → error 态 + 可重试;不外抛', () async { var attempts = 0; final auth = FakeAuthRepository( meHandler: () async { attempts += 1; if (attempts == 1) throw const ApiNetworkException('断网'); return buildProfile(nickname: '小柴'); }, ); final controller = build(auth); await controller.refresh(); expect(controller.phase, ProfileLoadPhase.error); expect(controller.lastError, isA()); expect(controller.profile, isNull); await controller.refresh(); expect(controller.phase, ProfileLoadPhase.ready); expect(controller.displayName, '小柴'); }); test('已有副本时刷新失败保留副本(不打断阅读,停在 ready)', () async { var attempts = 0; final controller = build( FakeAuthRepository( meHandler: () async { attempts += 1; if (attempts == 1) return buildProfile(nickname: '小柴'); throw const ApiNetworkException('断网'); }, ), ); await controller.refresh(); await controller.refresh(); expect(controller.phase, ProfileLoadPhase.ready); expect(controller.displayName, '小柴'); expect(controller.lastError, isA()); }); test('统计失败只降级统计块,资料仍 ready;refreshStats 可单独重试', () async { var statsAttempts = 0; community.onGetMyCommunityStats = () async { statsAttempts += 1; if (statsAttempts == 1) throw const ApiNetworkException('断网'); return CommunityStats.fromJson(sampleCommunityStatsJson()); }; community.onGetFollowStats = (_) async => FollowStats.fromJson(sampleFollowStatsJson()); final controller = build(FakeAuthRepository()); await controller.refresh(); expect(controller.phase, ProfileLoadPhase.ready); expect(controller.statsPhase, ProfileStatsPhase.error); expect(controller.communityStats, isNull); await controller.refreshStats(); expect(controller.statsPhase, ProfileStatsPhase.ready); expect(controller.phase, ProfileLoadPhase.ready); }); test('空数据:两个数字为 0(不是 null、不是 404)', () async { community.onGetMyCommunityStats = () async => CommunityStats.fromJson( sampleCommunityStatsJson(receivedLikeCount: 0, publishedPostCount: 0), ); community.onGetFollowStats = (_) async => FollowStats.fromJson( sampleFollowStatsJson(followerCount: 0, followingCount: 0), ); final controller = build(FakeAuthRepository()); await controller.refresh(); expect(controller.communityStats!.receivedLikeCount, 0); expect(controller.communityStats!.publishedPostCount, 0); }); test('save:空 patch 直接短路,不发请求(服务端对空 patch 答 400)', () async { final auth = FakeAuthRepository( updateMeHandler: (_) async => buildProfile(nickname: '不该被调用'), ); final controller = build(auth); await controller.refresh(); await controller.save(const UpdateMeRequest()); expect(auth.updateMePayloads, isEmpty); }); test('save 成功即以服务端回显替换副本(含 avatarUrl 现签值)', () async { final auth = FakeAuthRepository( updateMeHandler: (_) async => buildProfile( nickname: '小柴', avatarUrl: 'https://m/a.jpg?X-Amz-Signature=new', ), ); final controller = build(auth); await controller.refresh(); expect(controller.displayName, 'llx'); await controller.save( const UpdateMeRequest(nickname: PatchField.value('小柴')), ); expect(controller.displayName, '小柴'); expect(controller.profile!.avatarUrl, contains('X-Amz-Signature=new')); expect(auth.updateMePayloads.single, {'nickname': '小柴'}); }); test('save 失败按类型化异常外抛(供编辑页分层呈现),副本不动', () async { final auth = FakeAuthRepository( updateMeHandler: (_) async => throw const ApiBusinessException( code: ApiCodes.mediaNotReady, message: 'not ready', ), ); final controller = build(auth); await controller.refresh(); await expectLater( controller.save( const UpdateMeRequest(avatarAssetId: PatchField.value('a-1')), ), throwsA( isA().having( (e) => e.code, 'code', ApiCodes.mediaNotReady, ), ), ); expect(controller.displayName, 'llx'); }); test('reset:登出清空资料与统计,回 initial(跨账号不泄漏)', () async { final controller = build(FakeAuthRepository()); await controller.refresh(); expect(controller.profile, isNotNull); controller.reset(); expect(controller.phase, ProfileLoadPhase.initial); expect(controller.profile, isNull); expect(controller.displayName, isNull); expect(controller.communityStats, isNull); expect(controller.followStats, isNull); }); test('follow-stats 用的是本人 userId(路径主体正确)', () async { final controller = build( FakeAuthRepository(meHandler: () async => buildProfile(userId: 'u-42')), ); await controller.refresh(); expect(community.calls, contains('followStats:u-42')); expect(community.calls, contains('communityStats')); }); }