Files
photo-live/apps/mobile/test/pages/password_change_page_test.dart
T
2026-09-22 00:28:39 +08:00

165 lines
5.6 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 修改密码页 widget 测试(P3 计划 Task 3)。
///
/// 覆盖:客户端校验(新密码 ≥8、两次一致;不发请求)、旧密码错误
/// (401 AUTH_INVALID_CREDENTIALS)→「当前密码错误」文案且本地会话保留
/// (password-change 的 401 不驱逐)、成功 → toast + pop 回上级且
/// 会话令牌不变(服务端保留当前会话语义)。
library;
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:photo_live/core/api_client.dart';
import 'package:photo_live/core/error_codes.dart';
import 'package:photo_live/core/session_store.dart';
import 'package:photo_live/l10n/app_localizations.dart';
import 'package:photo_live/pages/password_change_page.dart';
import '../helpers/fakes.dart';
void main() {
late FakeSessionStore store;
late int unauthorizedCalls;
setUp(() {
store = FakeSessionStore();
unauthorizedCalls = 0;
});
ApiClient clientOf(FakeAdapter adapter) => ApiClient(
sessionStore: store,
dio: Dio(BaseOptions(baseUrl: 'http://test.local'))
..httpClientAdapter = adapter,
)..onUnauthorized = () => unauthorizedCalls++;
void seedSession() {
store.session = Session(
token: 'tok-live-1',
expiresAt: DateTime.utc(2099, 1, 1),
account: 'staff01',
);
}
Future<void> pumpOverMarker(WidgetTester tester, ApiClient client) async {
await tester.pumpWidget(
ProviderScope(
overrides: [apiClientProvider.overrideWith((ref) => client)],
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const Scaffold(body: Center(child: Text('MARK'))),
routes: {
'/password-change': (context) => const PasswordChangePage(),
},
),
),
);
tester.state<NavigatorState>(find.byType(Navigator).first)
.pushNamed('/password-change');
await tester.pumpAndSettle();
}
Future<void> fill(
WidgetTester tester, {
String oldPw = 'old-pass-123',
String newPw = 'brand-new-pass-9',
String confirmPw = 'brand-new-pass-9',
}) async {
await tester.enterText(
find.widgetWithText(TextFormField, 'Current password'),
oldPw,
);
await tester.enterText(
find.widgetWithText(TextFormField, 'New password'),
newPw,
);
await tester.enterText(
find.widgetWithText(TextFormField, 'Confirm new password'),
confirmPw,
);
}
testWidgets('新密码过短 → 客户端校验提示,不发请求', (tester) async {
seedSession();
final adapter = FakeAdapter((_) => throw StateError('不应发起请求'));
await pumpOverMarker(tester, clientOf(adapter));
await fill(tester, newPw: 'short7', confirmPw: 'short7');
await tester.tap(find.widgetWithText(FilledButton, 'Save password'));
await tester.pump();
expect(
find.text('New password must be at least 8 characters'),
findsOneWidget,
);
expect(adapter.requests, isEmpty);
});
testWidgets('两次新密码不一致 → 校验提示,不发请求', (tester) async {
seedSession();
final adapter = FakeAdapter((_) => throw StateError('不应发起请求'));
await pumpOverMarker(tester, clientOf(adapter));
await fill(tester, confirmPw: 'different-pass-9');
await tester.tap(find.widgetWithText(FilledButton, 'Save password'));
await tester.pump();
expect(
find.text('New passwords do not match'),
findsOneWidget,
);
expect(adapter.requests, isEmpty);
});
testWidgets('旧密码错误(401)→ 「当前密码错误」文案,会话保留不驱逐', (tester) async {
seedSession();
final adapter = FakeAdapter(
(o) => o.path == '/v1/auth/password-change'
? (401, errorBody(ErrorCodes.authInvalidCredentials))
: throw StateError('unexpected ${o.path}'),
);
await pumpOverMarker(tester, clientOf(adapter));
await fill(tester);
await tester.tap(find.widgetWithText(FilledButton, 'Save password'));
await tester.pumpAndSettle();
expect(find.text('Current password is incorrect'), findsOneWidget);
expect(store.session?.token, 'tok-live-1', reason: '旧密码错误不清会话');
expect(unauthorizedCalls, 0, reason: 'password-change 的 401 不驱逐');
expect(find.text('MARK'), findsNothing, reason: '停留在修改密码页');
});
testWidgets('成功 → toast + pop 回上级;会话令牌不变(当前会话保留)', (tester) async {
seedSession();
final adapter = FakeAdapter(
(o) => o.path == '/v1/auth/password-change'
? (204, null)
: throw StateError('unexpected ${o.path}'),
);
await pumpOverMarker(tester, clientOf(adapter));
await fill(tester);
await tester.tap(find.widgetWithText(FilledButton, 'Save password'));
await tester.pumpAndSettle();
final req = adapter.requests.single;
expect(req.method, 'POST');
expect(req.path, '/v1/auth/password-change');
expect(req.headers['Authorization'], 'Bearer tok-live-1');
expect(req.body, {
'oldPassword': 'old-pass-123',
'newPassword': 'brand-new-pass-9',
});
expect(find.text('Password changed'), findsOneWidget);
expect(find.text('MARK'), findsOneWidget, reason: '成功后 pop 回上级');
expect(
store.session?.token,
'tok-live-1',
reason: '服务端保留当前会话:本地令牌无需变更',
);
});
}