import 'package:patbond_flutter/features/community/community_models.dart'; import 'package:patbond_flutter/features/community/community_repository.dart'; /// community 域测试样本 JSON(字段与契约 v1.3.0 逐字一致)。 Map sampleAuthorJson({ String userId = 'u-1', String? nickname = '毛毛的铲屎官', String? avatarUrl = 'https://minio.local/avatar.jpg?X-Amz-Signature=sig', }) => {'userId': userId, 'nickname': nickname, 'avatarUrl': avatarUrl}; Map samplePostMediaItemJson({ String assetId = 'a-1', int position = 0, bool isCover = true, }) => { 'assetId': assetId, 'position': position, 'isCover': isCover, 'url': 'https://minio.local/p.jpg?X-Amz-Signature=sig', 'widthPx': 1080, 'heightPx': 810, 'caption': '晒太阳', }; Map samplePostJson({ String id = 'p-1', String status = 'published', bool likedByMe = false, int likeCount = 6, bool bookmarkedByMe = false, int bookmarkCount = 2, int version = 1, }) => { 'id': id, 'author': sampleAuthorJson(), 'petId': 'pet-1', 'category': 'general', 'title': '今天的豆豆', 'content': '晒了一下午太阳。', 'status': status, 'visibility': 'public', 'media': [samplePostMediaItemJson()], 'likeCount': likeCount, 'commentCount': 3, 'bookmarkCount': bookmarkCount, 'likedByMe': likedByMe, 'bookmarkedByMe': bookmarkedByMe, 'createdAt': '2026-09-08T10:00:00.000Z', 'updatedAt': '2026-09-08T10:05:00.000Z', 'publishedAt': status == 'published' ? '2026-09-08T10:05:00.000Z' : null, 'version': version, }; Map sampleFeedCardJson({ String id = 'p-1', bool likedByMe = false, int likeCount = 6, bool bookmarkedByMe = false, int bookmarkCount = 2, }) => { 'id': id, 'author': sampleAuthorJson(), 'category': 'general', 'title': '今天的豆豆', 'contentPreview': '晒了一下午太阳。', 'coverImage': samplePostMediaItemJson(), 'mediaCount': 1, 'likeCount': likeCount, 'commentCount': 3, 'bookmarkCount': bookmarkCount, 'likedByMe': likedByMe, 'bookmarkedByMe': bookmarkedByMe, 'publishedAt': '2026-09-08T10:05:00.000Z', }; Map sampleCommentJson({ String id = 'c-1', Map? author, Map? replyToUser, String content = '好可爱!', }) => { 'id': id, 'postId': 'p-1', 'author': author ?? sampleAuthorJson(), 'replyToUser': replyToUser, 'content': content, 'createdAt': '2026-09-08T11:00:00.000Z', }; Map sampleUploadCredentialsJson() => { 'assetId': 'a-1', 'uploadUrl': 'https://minio.local/patbond-media/post_image/a-1?X-Amz-Signature=sig', 'method': 'PUT', 'requiredHeaders': {'Content-Type': 'image/jpeg'}, 'expiresAt': '2026-09-08T10:10:00.000Z', }; Map sampleMediaAssetJson({String status = 'ready'}) => { 'id': 'a-1', 'kind': 'image', 'purpose': 'post_image', 'mimeType': 'image/jpeg', 'byteSize': 204800, 'widthPx': 1080, 'heightPx': 810, 'status': status, 'url': status == 'ready' ? 'https://minio.local/p.jpg?X-Amz-Signature=sig' : null, 'readyAt': status == 'ready' ? '2026-09-08T10:06:00.000Z' : null, 'createdAt': '2026-09-08T10:00:00.000Z', }; Map cursorPageJson( List> items, { String? nextCursor, bool hasMore = false, }) => {'items': items, 'nextCursor': nextCursor, 'hasMore': hasMore}; FeedCard sampleFeedCard({ String id = 'p-1', bool likedByMe = false, int likeCount = 6, }) => FeedCard.fromJson( sampleFeedCardJson(id: id, likedByMe: likedByMe, likeCount: likeCount), ); CursorPage feedPage( List items, { String? nextCursor, bool hasMore = false, }) => CursorPage(items: items, nextCursor: nextCursor, hasMore: hasMore); Post samplePost({ String id = 'p-1', bool likedByMe = false, int likeCount = 6, bool bookmarkedByMe = false, int bookmarkCount = 2, }) => Post.fromJson( samplePostJson( id: id, likedByMe: likedByMe, likeCount: likeCount, bookmarkedByMe: bookmarkedByMe, bookmarkCount: bookmarkCount, ), ); PostComment sampleComment({ String id = 'c-1', Map? author, Map? replyToUser, String content = '好可爱!', }) => PostComment.fromJson( sampleCommentJson( id: id, author: author, replyToUser: replyToUser, content: content, ), ); CursorPage commentPage( List items, { String? nextCursor, bool hasMore = false, }) => CursorPage(items: items, nextCursor: nextCursor, hasMore: hasMore); /// 草稿样本(T3-17 发布页草稿恢复用;media 可清空为纯文字草稿)。 Post sampleDraft({ String id = 'draft-1', int version = 3, String content = '草稿正文', bool withMedia = true, }) => Post.fromJson({ ...samplePostJson(id: id, status: 'draft', version: version), 'content': content, if (!withMedia) 'media': const >[], }); CursorPage postPage(List items) => CursorPage(items: items, nextCursor: null, hasMore: false); /// 假仓库:controller 测试注入行为并记录调用(Completer 控时序)。 /// 未注入 handler 的方法一律 UnimplementedError(误触发即测试失败)。 class FakeCommunityRepository implements CommunityRepository { /// 调用日志,如 `feed:cursor=null`、`like:p-1`、`unlike:p-1`。 final List calls = []; Future> Function(int? limit, String? cursor)? onFeed; Future Function(String postId, bool target)? onLikeToggle; Future Function(String postId, bool target)? onBookmarkToggle; Future Function(String postId)? onGetPost; Future> Function(String postId, String? cursor)? onListComments; Future Function(String postId, CreateCommentRequest request)? onCreateComment; Future Function(String commentId)? onDeleteComment; Future Function(String userId, bool target)? onFollowToggle; Future Function(String userId)? onGetFollowStats; Future Function()? onGetMyCommunityStats; Future Function(CreateMediaUploadRequest request)? onCreateMediaUpload; Future Function(String assetId)? onCompleteMediaUpload; Future Function(CreatePostRequest request, String? idempotencyKey)? onCreatePost; Future Function(String postId, UpdatePostRequest request)? onUpdatePost; Future Function(String postId)? onDeletePost; Future> Function(PostStatus? status)? onListMyPosts; /// createPost 收到的幂等键顺序(同键重放断言用)。 final List idempotencyKeys = []; /// 最近一次 createMediaUpload 的请求(purpose 断言用)。 CreateMediaUploadRequest? lastMediaUploadRequest; @override Future> getFeed({int? limit, String? cursor}) { calls.add('feed:cursor=$cursor'); return onFeed!(limit, cursor); } @override Future likePost(String postId) { calls.add('like:$postId'); return onLikeToggle!(postId, true); } @override Future unlikePost(String postId) { calls.add('unlike:$postId'); return onLikeToggle!(postId, false); } @override Future bookmarkPost(String postId) { calls.add('bookmark:$postId'); return onBookmarkToggle!(postId, true); } @override Future unbookmarkPost(String postId) { calls.add('unbookmark:$postId'); return onBookmarkToggle!(postId, false); } @override Future getPost(String postId) { calls.add('getPost:$postId'); return onGetPost!(postId); } @override Future createMediaUpload( CreateMediaUploadRequest request, ) { calls.add('createUpload:${request.mimeType}:${request.byteSize}'); lastMediaUploadRequest = request; return onCreateMediaUpload!(request); } @override Future completeMediaUpload(String assetId) { calls.add('complete:$assetId'); return onCompleteMediaUpload!(assetId); } @override Future createPost(CreatePostRequest request, {String? idempotencyKey}) { calls.add('createPost:${request.status?.name}:${request.content}'); idempotencyKeys.add(idempotencyKey); return onCreatePost!(request, idempotencyKey); } @override Future updatePost(String postId, UpdatePostRequest request) { calls.add( 'updatePost:$postId:v${request.version}:publish=${request.publish}', ); return onUpdatePost!(postId, request); } @override Future deletePost(String postId) { calls.add('deletePost:$postId'); return onDeletePost!(postId); } @override Future> listMyPosts({ int? limit, String? cursor, PostStatus? status, }) { calls.add('listMyPosts:${status?.name}'); return onListMyPosts == null ? Future.value( const CursorPage(items: [], nextCursor: null, hasMore: false), ) : onListMyPosts!(status); } @override Future> listComments( String postId, { int? limit, String? cursor, }) { calls.add('comments:$postId:cursor=$cursor'); return onListComments!(postId, cursor); } @override Future createComment( String postId, CreateCommentRequest request, ) { calls.add('createComment:$postId:${request.content}'); return onCreateComment!(postId, request); } @override Future deleteComment(String commentId) { calls.add('deleteComment:$commentId'); return onDeleteComment!(commentId); } @override Future followUser(String userId) { calls.add('follow:$userId'); return onFollowToggle!(userId, true); } @override Future unfollowUser(String userId) { calls.add('unfollow:$userId'); return onFollowToggle!(userId, false); } @override Future getFollowStats(String userId) { calls.add('followStats:$userId'); return onGetFollowStats?.call(userId) ?? Future.value(FollowStats.fromJson(sampleFollowStatsJson())); } @override Future getMyCommunityStats() { calls.add('communityStats'); return onGetMyCommunityStats?.call() ?? // 缺省零值(契约:空数据返回 0 而非 null,且永不 404)。 Future.value( CommunityStats.fromJson( sampleCommunityStatsJson( receivedLikeCount: 0, publishedPostCount: 0, ), ), ); } @override Future> listMyBookmarks({int? limit, String? cursor}) => throw UnimplementedError(); } Map sampleCommunityStatsJson({ int receivedLikeCount = 128, int publishedPostCount = 12, }) => { 'receivedLikeCount': receivedLikeCount, 'publishedPostCount': publishedPostCount, }; Map sampleFollowStatsJson({ int followerCount = 24, int followingCount = 7, bool followedByMe = false, }) => { 'followerCount': followerCount, 'followingCount': followingCount, 'followedByMe': followedByMe, };