feat(account): 增加账号中心页面逻辑

This commit is contained in:
Zhou Weidong committed 2026-10-19 09:48:07 +08:00
1 parent ef7f4ce3b6
commit c68f02449f
6 files changed
+503

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
// Author: fruit.com.cn
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/data/models/account_device.dart';
import 'package:fruitshorts/core/data/models/account_profile.dart';
import 'package:fruitshorts/pages/account/account_deactivate_logic.dart';
import 'package:fruitshorts/pages/account/account_devices_logic.dart';
import 'package:fruitshorts/pages/account/account_export_logic.dart';
import 'package:fruitshorts/pages/account/account_profile_logic.dart';
import '../support/fake_account_repository.dart';
void main() {
test('should_update_profile_and_notify_auth_when_saved', () async {
final repository = FakeAccountRepository();
AccountProfile? applied;
final logic = AccountProfileLogic(
repository,
onProfileUpdated: (profile) async => applied = profile,
);
await logic.load();
final ok = await logic.save(
nickname: ' Nadia Baru ',
gender: 'unknown',
birthday: '1999-01-02',
);
expect(ok, isTrue);
expect(repository.lastUpdate?.nickname, 'Nadia Baru');
expect(repository.lastUpdate?.gender, 'unknown');
expect(repository.lastUpdate?.birthday, '1999-01-02');
expect(applied?.nickname, 'Nadia Baru');
});
test('should_load_devices_and_keep_the_page_read_only', () async {
final repository = FakeAccountRepository(
devices: const [
AccountDevice(
id: '1',
deviceId: 'd-device-1234',
platform: 'IOS',
deviceModel: 'iPhone 15',
status: 'active',
),
],
);
final logic = AccountDevicesLogic(repository);
await logic.load();
expect(logic.devices.single.deviceModel, 'iPhone 15');
expect(logic.loading, isFalse);
expect(logic.error, isNull);
});
test('should_copy_complete_export_json_when_requested', () async {
final repository = FakeAccountRepository();
String copied = '';
final logic = AccountExportLogic(
repository,
copyText: (value) async => copied = value,
);
await logic.load();
final ok = await logic.copy();
expect(ok, isTrue);
expect(copied, contains('"credentials"'));
expect(copied, contains('n***@example.com'));
});
test('should_clear_session_and_keep_schedule_when_deactivation_succeeds', () async {
final repository = FakeAccountRepository();
var cleared = false;
final logic = AccountDeactivateLogic(
repository,
clearSession: () async => cleared = true,
);
final ok = await logic.request('不再使用');
expect(ok, isTrue);
expect(repository.lastDeactivateReason, '不再使用');
expect(logic.scheduledAt, DateTime(2026, 11, 3, 9, 30));
expect(cleared, isTrue);
});
test('should_show_backend_message_when_cancel_deactivation_fails', () async {
final repository = FakeAccountRepository()..failCancel = true;
final logic = AccountDeactivateLogic(
repository,
clearSession: () async {},
);
final ok = await logic.cancel();
expect(ok, isFalse);
expect(logic.actionMessage, '没有待处理的注销申请');
});
}
+127
View File
@@ -0,0 +1,127 @@
// Author: fruit.com.cn
import 'package:fruitshorts/core/data/models/account_device.dart';
import 'package:fruitshorts/core/data/models/account_export.dart';
import 'package:fruitshorts/core/data/models/account_profile.dart';
import 'package:fruitshorts/core/data/repositories/account_repository.dart';
import 'package:fruitshorts/core/data/remote/api_client.dart';
/// 账号中心测试替身;可预置数据与失败文案。
class FakeAccountRepository implements AccountRepository {
FakeAccountRepository({
AccountProfile? profile,
List<AccountDevice> devices = const [],
AccountExport? export,
}) : _profile = profile ?? defaultProfile,
_devices = [...devices],
_export = export ?? defaultExport;
static const defaultProfile = AccountProfile(
uid: 'U-K19',
nickname: 'Nadia',
avatarUrl: '',
gender: 'female',
countryCode: 'ID',
regionCode: 'JK',
language: 'id',
timezone: 'Asia/Jakarta',
currency: 'IDR',
status: 'normal',
maskedTarget: 'n***@example.com',
vipLevel: 0,
);
static const defaultExport = AccountExport(
profile: defaultProfile,
credentials: [
AccountCredential(
type: 'EMAIL',
masked: 'n***@example.com',
verified: true,
),
],
consentCount: 1,
devices: [],
raw: {
'profile': {'uid': 'U-K19', 'nickname': 'Nadia'},
'credentials': [
{'type': 'EMAIL', 'masked': 'n***@example.com', 'verified': true},
],
'consents': [
{'scene': 'privacy'},
],
'devices': [],
},
);
AccountProfile _profile;
List<AccountDevice> _devices;
AccountExport _export;
AccountProfileUpdate? lastUpdate;
String? lastDeactivateReason;
int profileCalls = 0;
int deviceCalls = 0;
int exportCalls = 0;
bool failProfile = false;
bool failDevices = false;
bool failExport = false;
bool failUpdate = false;
bool failDeactivate = false;
bool failCancel = false;
@override
Future<AccountProfile> profile() async {
profileCalls++;
if (failProfile) throw const ApiException(500, '资料加载失败');
return _profile;
}
@override
Future<AccountProfile> updateProfile(AccountProfileUpdate update) async {
lastUpdate = update;
if (failUpdate) throw const ApiException(400, '昵称不可用');
_profile = AccountProfile(
uid: _profile.uid,
nickname: update.nickname ?? _profile.nickname,
avatarUrl: _profile.avatarUrl,
gender: update.gender ?? _profile.gender,
countryCode: _profile.countryCode,
regionCode: _profile.regionCode,
language: _profile.language,
timezone: _profile.timezone,
currency: _profile.currency,
status: _profile.status,
maskedTarget: _profile.maskedTarget,
vipLevel: _profile.vipLevel,
);
return _profile;
}
@override
Future<List<AccountDevice>> devices() async {
deviceCalls++;
if (failDevices) throw const ApiException(500, '设备加载失败');
return [..._devices];
}
@override
Future<AccountExport> export() async {
exportCalls++;
if (failExport) throw const ApiException(500, '导出失败');
return _export;
}
@override
Future<DateTime?> deactivate(String reason) async {
lastDeactivateReason = reason;
if (failDeactivate) throw const ApiException(100009, '注销申请已在处理中');
return DateTime(2026, 11, 3, 9, 30);
}
@override
Future<void> cancelDeactivation() async {
if (failCancel) {
throw const ApiException(100009, '没有待处理的注销申请');
}
}
}