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/profile/profile_display.dart'; import '../../helpers/auth_test_helpers.dart'; void main() { group('展示名回退 nickname ?? username', () { test('有昵称:展示昵称,nickname 字段保留 DB 原值', () { final profile = buildProfile(username: 'llx', nickname: '小柴'); expect(profile.displayName, '小柴'); expect(profile.nickname, '小柴'); }); test('无昵称:展示 username,但 nickname 仍为 null(不被回退值污染)', () { final profile = buildProfile(username: 'llx'); expect(profile.displayName, 'llx'); expect( profile.nickname, isNull, reason: '回退只在展示层发生;nickname 必须保持 null,否则编辑页预填后会被固化成真昵称', ); }); test('hasAvatar 只看 avatarUrl(响应不外露 avatarAssetId)', () { expect(buildProfile().hasAvatar, isFalse); expect( buildProfile(avatarUrl: 'https://m/a.jpg?X-Amz-Signature=s').hasAvatar, isTrue, ); }); }); group('UpdateMeRequest 三态载荷', () { test('什么都没碰 → isEmpty 且 JSON 为空对象', () { const request = UpdateMeRequest(); expect(request.isEmpty, isTrue); expect(request.toJson(), isEmpty); }); test('只改昵称:avatarAssetId 键**不出现**(不改,而不是清空)', () { const request = UpdateMeRequest(nickname: PatchField.value('小柴')); final json = request.toJson(); expect(json, {'nickname': '小柴'}); expect( json.containsKey('avatarAssetId'), isFalse, reason: '发成 avatarAssetId: null 会把用户头像一起清掉', ); }); test('只清昵称:nickname 显式 null,头像键仍不出现', () { const request = UpdateMeRequest(nickname: PatchField.clear()); final json = request.toJson(); expect(json.containsKey('nickname'), isTrue); expect(json['nickname'], isNull); expect(json.containsKey('avatarAssetId'), isFalse); }); test('一次改两样:两键齐出', () { const request = UpdateMeRequest( nickname: PatchField.value('小柴'), avatarAssetId: PatchField.value('asset-9'), ); expect(request.toJson(), {'nickname': '小柴', 'avatarAssetId': 'asset-9'}); expect(request.isEmpty, isFalse); }); test('只清头像:nickname 键不出现', () { const request = UpdateMeRequest( avatarAssetId: PatchField.clear(), ); final json = request.toJson(); expect(json.containsKey('nickname'), isFalse); expect(json['avatarAssetId'], isNull); }); }); group('昵称校验与 ck_users_nickname 对齐', () { test('1 码点通过;32 CJK 通过;33 拒', () { expect(validateNickname('柴'), isNull); expect(validateNickname('柴' * 32), isNull); expect(validateNickname('柴' * 33), isNotNull); }); test('32 个 emoji(UTF-16 长度 64)通过——长度按码点计而非 String.length', () { const emoji = '🐕'; final name = emoji * 32; expect(name.length, 64, reason: '前提:UTF-16 长度确为 64'); expect( validateNickname(name), isNull, reason: '按 String.length 校验会误拒数据库能存的 32 emoji 昵称', ); expect(validateNickname(emoji * 33), isNotNull); }); test('btrim 对齐:前后空格不计入长度', () { expect(validateNickname(' 豆豆 '), isNull); expect(validateNickname(' ${'柴' * 32} '), isNull); }); test('纯空白不是隐式清空,而是校验失败并指向显式清除入口', () { final message = validateNickname(' '); expect(message, isNotNull); expect(message, contains('清除昵称')); }); }); group('错误文案分层', () { test('保存失败按 PATCH /me 错误谱分层', () { expect( profileSaveErrorMessage( const ApiBusinessException(code: ApiCodes.paramError, message: 'x'), ), '填写内容不符合要求,请检查后重试', ); expect( profileSaveErrorMessage( const ApiBusinessException( code: ApiCodes.mediaNotFound, message: 'x', ), ), '头像已失效,请重新上传', ); expect( profileSaveErrorMessage( const ApiBusinessException( code: ApiCodes.mediaNotReady, message: 'x', ), ), '头像还没上传完,请稍后重试', ); expect( profileSaveErrorMessage(const ApiNetworkException()), '网络异常,请检查网络后重试', ); }); test('加载失败文案不透出服务端 message', () { expect( profileLoadErrorMessage( const ApiNetworkException('Connection refused'), ), '网络异常,请检查网络后重试', ); expect(profileLoadErrorMessage(null), '加载失败,请稍后重试'); }); }); }