import 'package:flutter_test/flutter_test.dart'; import 'package:patbond_flutter/features/community/community_models.dart'; import '../../helpers/community_test_helpers.dart'; void main() { group('响应 DTO 逐字段映射(契约 v1.3.0)', () { test('Post:完整形态含作者 / media / 互动计数 / publishedAt', () { final post = Post.fromJson(samplePostJson()); expect(post.id, 'p-1'); expect(post.author.userId, 'u-1'); expect(post.author.nickname, '毛毛的铲屎官'); expect(post.petId, 'pet-1'); expect(post.category, PostCategory.general); expect(post.title, '今天的豆豆'); expect(post.content, '晒了一下午太阳。'); expect(post.status, PostStatus.published); expect(post.visibility, PostVisibility.public); expect(post.media.single.assetId, 'a-1'); expect(post.media.single.isCover, isTrue); expect(post.likeCount, 6); expect(post.commentCount, 3); expect(post.bookmarkCount, 2); expect(post.likedByMe, isFalse); expect(post.bookmarkedByMe, isFalse); expect(post.publishedAt, isNotNull); expect(post.version, 1); }); test('Post:draft 态 publishedAt 为 null(仅 published 非空)', () { final post = Post.fromJson(samplePostJson(status: 'draft')); expect(post.status, PostStatus.draft); expect(post.publishedAt, isNull); }); test('FeedCard:卡片裁剪形态,纯文字帖 coverImage 为 null', () { final withCover = FeedCard.fromJson(sampleFeedCardJson()); expect(withCover.coverImage!.url, contains('X-Amz-Signature')); expect(withCover.mediaCount, 1); expect(withCover.contentPreview, '晒了一下午太阳。'); expect(withCover.publishedAt, DateTime.parse('2026-09-08T10:05:00.000Z')); final textOnly = FeedCard.fromJson( sampleFeedCardJson() ..['coverImage'] = null ..['mediaCount'] = 0, ); expect(textOnly.coverImage, isNull); expect(textOnly.mediaCount, 0); }); test('category:ai_creation(读侧预留)按线上 snake_case 解析', () { final card = FeedCard.fromJson( sampleFeedCardJson()..['category'] = 'ai_creation', ); expect(card.category, PostCategory.aiCreation); expect(PostCategory.aiCreation.wire, 'ai_creation'); }); test('AuthorSummary:nickname 与 avatarUrl 同为 null 即降级/墓碑形态', () { final normal = AuthorSummary.fromJson(sampleAuthorJson()); expect(normal.isDegraded, isFalse); final degraded = AuthorSummary.fromJson( sampleAuthorJson(nickname: null, avatarUrl: null), ); expect(degraded.userId, 'u-1'); expect(degraded.isDegraded, isTrue); // 仅缺头像不算降级(无头像 / 头像非 ready 也是 null)。 final noAvatar = AuthorSummary.fromJson( sampleAuthorJson(avatarUrl: null), ); expect(noAvatar.isDegraded, isFalse); }); test('PostComment:replyToUser 非回复为 null,@ 回复含降级形态', () { final plain = PostComment.fromJson(sampleCommentJson()); expect(plain.replyToUser, isNull); expect(plain.postId, 'p-1'); final reply = PostComment.fromJson( sampleCommentJson( replyToUser: sampleAuthorJson( userId: 'u-2', nickname: null, avatarUrl: null, ), ), ); expect(reply.replyToUser!.userId, 'u-2'); expect(reply.replyToUser!.isDegraded, isTrue); }); test('MediaUploadCredentials:requiredHeaders 原样映射为字符串表', () { final credentials = MediaUploadCredentials.fromJson( sampleUploadCredentialsJson(), ); expect(credentials.assetId, 'a-1'); expect(credentials.method, 'PUT'); expect(credentials.requiredHeaders, {'Content-Type': 'image/jpeg'}); expect(credentials.expiresAt, DateTime.parse('2026-09-08T10:10:00.000Z')); }); test('MediaAsset:ready 态 url/readyAt 非空,uploading 态为 null', () { final ready = MediaAsset.fromJson(sampleMediaAssetJson()); expect(ready.status, MediaAssetStatus.ready); expect(ready.url, isNotNull); expect(ready.readyAt, isNotNull); expect(ready.byteSize, 204800); final uploading = MediaAsset.fromJson( sampleMediaAssetJson(status: 'uploading'), ); expect(uploading.status, MediaAssetStatus.uploading); expect(uploading.url, isNull); expect(uploading.readyAt, isNull); }); test('LikeState / BookmarkState / FollowState / FollowStats 终态解析', () { final like = LikeState.fromJson({'liked': true, 'likeCount': 7}); expect(like.liked, isTrue); expect(like.likeCount, 7); final bookmark = BookmarkState.fromJson({ 'bookmarked': false, 'bookmarkCount': 0, }); expect(bookmark.bookmarked, isFalse); expect(bookmark.bookmarkCount, 0); final follow = FollowState.fromJson({ 'following': true, 'followerCount': 12, }); expect(follow.following, isTrue); expect(follow.followerCount, 12); final stats = FollowStats.fromJson({ 'followerCount': 12, 'followingCount': 34, 'followedByMe': false, }); expect(stats.followingCount, 34); expect(stats.followedByMe, isFalse); }); test('未知枚举取值抛 FormatException(契约漂移测试期暴露)', () { expect( () => Post.fromJson(samplePostJson()..['status'] = 'hidden'), throwsFormatException, ); expect( () => FeedCard.fromJson(sampleFeedCardJson()..['category'] = 'topic'), throwsFormatException, ); expect( () => MediaAsset.fromJson(sampleMediaAssetJson(status: 'deleted')), throwsFormatException, ); expect( () => Post.fromJson(samplePostJson()..['visibility'] = 'private'), throwsFormatException, ); }); }); group('请求 DTO 序列化', () { test('CreatePostRequest:可选字段缺席不出现,media 按项序列化', () { const minimal = CreatePostRequest(content: '纯文字帖'); expect(minimal.toJson(), {'content': '纯文字帖'}); const full = CreatePostRequest( content: '正文', title: '标题', category: PostCategory.help, status: PostStatus.published, petId: 'pet-1', media: [ PostMediaAttachRequest(assetId: 'a-1', position: 0, isCover: true), PostMediaAttachRequest(assetId: 'a-2', position: 1, caption: '第二张'), ], ); expect(full.toJson(), { 'content': '正文', 'title': '标题', 'category': 'help', 'status': 'published', 'petId': 'pet-1', 'media': [ {'assetId': 'a-1', 'position': 0, 'isCover': true}, {'assetId': 'a-2', 'position': 1, 'caption': '第二张'}, ], }); }); test('UpdatePostRequest:media 三态——缺席不动 / [] 清空 / 非空整组替换', () { const absent = UpdatePostRequest(version: 2, title: '改标题'); expect(absent.toJson(), {'version': 2, 'title': '改标题'}); expect(absent.toJson().containsKey('media'), isFalse); const clear = UpdatePostRequest(version: 2, media: []); expect(clear.toJson(), {'version': 2, 'media': []}); const replace = UpdatePostRequest( version: 2, media: [PostMediaAttachRequest(assetId: 'a-3')], ); expect(replace.toJson()['media'], [ {'assetId': 'a-3'}, ]); }); test('UpdatePostRequest:publish 即 status: published(唯一开放迁移)', () { const publish = UpdatePostRequest(version: 1, publish: true); expect(publish.toJson(), {'version': 1, 'status': 'published'}); const noPublish = UpdatePostRequest(version: 1, content: '改正文'); expect(noPublish.toJson().containsKey('status'), isFalse); }); test('CreateMediaUploadRequest:kind/purpose 按线上取值,sha256 可选', () { const request = CreateMediaUploadRequest( kind: MediaKind.image, purpose: MediaPurpose.postImage, mimeType: 'image/jpeg', byteSize: 204800, ); expect(request.toJson(), { 'kind': 'image', 'purpose': 'post_image', 'mimeType': 'image/jpeg', 'byteSize': 204800, }); const withHash = CreateMediaUploadRequest( kind: MediaKind.image, purpose: MediaPurpose.postImage, mimeType: 'image/webp', byteSize: 1, sha256: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', ); expect(withHash.toJson()['sha256'], hasLength(64)); }); test('CreateCommentRequest:replyToUserId 缺席不出现', () { expect(const CreateCommentRequest(content: '好可爱!').toJson(), { 'content': '好可爱!', }); expect( const CreateCommentRequest( content: '@回复', replyToUserId: 'u-2', ).toJson(), {'content': '@回复', 'replyToUserId': 'u-2'}, ); }); }); group('互动字段副本更新', () { test('Post.copyWithInteraction 只动互动字段', () { final post = Post.fromJson(samplePostJson()); final updated = post.copyWithInteraction(likedByMe: true, likeCount: 7); expect(updated.likedByMe, isTrue); expect(updated.likeCount, 7); expect(updated.bookmarkCount, post.bookmarkCount); expect(updated.content, post.content); expect(updated.version, post.version); }); test('FeedCard.copyWithInteraction 只动互动字段', () { final card = sampleFeedCard(); final updated = card.copyWithInteraction( bookmarkedByMe: true, bookmarkCount: 3, ); expect(updated.bookmarkedByMe, isTrue); expect(updated.bookmarkCount, 3); expect(updated.likeCount, card.likeCount); expect(updated.contentPreview, card.contentPreview); }); }); }