缺失文件
parent
f9f66882ae
commit
38c2d3c7fe
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
|
|
@ -0,0 +1,178 @@
|
||||||
|
# Forced Default Password Change Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Require users who log in with `Cc@12345678` to change that default password from a non-dismissible home-page dialog and then sign in again.
|
||||||
|
|
||||||
|
**Architecture:** Add a pure password-policy utility for the default-password trigger and the 8–16-character validation rule. Pass the login password through every successful login path to `MainPage`; it displays a reusable forced-change dialog after the first frame, calls the existing password API, and uses centralized logout to remove all credentials and session state.
|
||||||
|
|
||||||
|
**Tech Stack:** Flutter, Dart, `flutter_test`, `SharedPreferences`, existing `AuthApi` and `SessionService`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add the password-policy utility
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `lib/services/password_policy.dart`
|
||||||
|
- Create: `test/services/password_policy_test.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing policy tests**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
test('recognizes the configured default password', () {
|
||||||
|
expect(PasswordPolicy.requiresChange('Cc@12345678'), isTrue);
|
||||||
|
expect(PasswordPolicy.requiresChange('Cc@12345679'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts a valid password and rejects invalid character sets', () {
|
||||||
|
expect(PasswordPolicy.validate('Aa@12345'), isNull);
|
||||||
|
expect(PasswordPolicy.validate('Aa$12345'), isNotNull);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the policy tests to verify failure**
|
||||||
|
|
||||||
|
Run: `flutter test test/services/password_policy_test.dart`
|
||||||
|
|
||||||
|
Expected: FAIL because `PasswordPolicy` does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the policy**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
class PasswordPolicy {
|
||||||
|
static const defaultPassword = 'Cc@12345678';
|
||||||
|
|
||||||
|
static bool requiresChange(String? password) => password == defaultPassword;
|
||||||
|
|
||||||
|
static String? validate(String password) {
|
||||||
|
final valid = RegExp(r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!#@*&])[A-Za-z\d!#@*&]{8,16}$');
|
||||||
|
return valid.hasMatch(password) ? null : '密码长度为8-16位,且必须包含大写字母、小写字母、数字和特殊符号(!#@*&)。';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the policy tests to verify success**
|
||||||
|
|
||||||
|
Run: `flutter test test/services/password_policy_test.dart`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 2: Add the reusable forced-change dialog
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `lib/customWidget/custom_alert_dialog.dart`
|
||||||
|
- Create: `lib/pages/mine/force_change_password_dialog.dart`
|
||||||
|
- Create: `test/pages/mine/force_change_password_dialog_test.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing widget tests**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
await ForceChangePasswordDialog.show(
|
||||||
|
context,
|
||||||
|
oldPassword: 'Cc@12345678',
|
||||||
|
onUpdatePassword: (_, __) async => true,
|
||||||
|
);
|
||||||
|
expect(find.text('请修改默认密码'), findsOneWidget);
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the dialog cannot be dismissed by the back action, rejects invalid passwords without calling the updater, and returns success after a valid update.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the widget test to verify failure**
|
||||||
|
|
||||||
|
Run: `flutter test test/pages/mine/force_change_password_dialog_test.dart`
|
||||||
|
|
||||||
|
Expected: FAIL because the dialog API does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement dialog and alert extension**
|
||||||
|
|
||||||
|
Add optional `customContent` and `onConfirmAsync` fields to `CustomAlertDialog`. Build the three password fields inside `ForceChangePasswordDialog`; use `force: true`, `PasswordPolicy.validate`, and an injected updater so the modal remains visible for validation or API failures.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the widget test to verify success**
|
||||||
|
|
||||||
|
Run: `flutter test test/pages/mine/force_change_password_dialog_test.dart`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 3: Trigger forced change after every login path
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `lib/pages/main_tab.dart`
|
||||||
|
- Modify: `lib/pages/user/login_page.dart`
|
||||||
|
- Modify: `lib/pages/user/choose_userFirm_page.dart`
|
||||||
|
- Create: `test/pages/main_tab_forced_password_change_test.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write a failing trigger test**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
expect(MainPage.shouldForcePasswordChange('Cc@12345678'), isTrue);
|
||||||
|
expect(MainPage.shouldForcePasswordChange('new-password'), isFalse);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the trigger test to verify failure**
|
||||||
|
|
||||||
|
Run: `flutter test test/pages/main_tab_forced_password_change_test.dart`
|
||||||
|
|
||||||
|
Expected: FAIL because `MainPage.shouldForcePasswordChange` does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the trigger and logout flow**
|
||||||
|
|
||||||
|
Give `MainPage` optional `loginPassword` constructor data. Schedule the forced dialog after the first frame only when `PasswordPolicy.requiresChange(loginPassword)` is true. Submit `id`, current password, and new password through `AuthApi.changePassWord`; after success call `AuthService.logout()` and replace the route stack with `LoginPage`. Pass the entered password through direct login and selected-firm login.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the trigger test to verify success**
|
||||||
|
|
||||||
|
Run: `flutter test test/pages/main_tab_forced_password_change_test.dart`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 4: Reuse policy and centralized logout in manual password changes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `lib/pages/mine/mine_set_pwd_page.dart`
|
||||||
|
- Modify: `lib/services/auth_service.dart`
|
||||||
|
- Test: `test/services/password_policy_test.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Extend policy tests for length boundaries**
|
||||||
|
|
||||||
|
```dart
|
||||||
|
test('rejects passwords outside the 8 to 16 character range', () {
|
||||||
|
expect(PasswordPolicy.validate('Aa@1234'), isNotNull);
|
||||||
|
expect(PasswordPolicy.validate('Aa@12345678901234'), isNotNull);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run policy tests to verify failure**
|
||||||
|
|
||||||
|
Run: `flutter test test/services/password_policy_test.dart`
|
||||||
|
|
||||||
|
Expected: FAIL until exact length checking is implemented.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Share validation and credential cleanup**
|
||||||
|
|
||||||
|
Replace `MineSetPwdPage`’s local validation with `PasswordPolicy.validate`. Change `AuthService.logout()` to remove `savePhone` and `savePass` in addition to its existing state, then use it after any successful manual password change.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run policy tests to verify success**
|
||||||
|
|
||||||
|
Run: `flutter test test/services/password_policy_test.dart`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 5: Format and verify
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: all changed Dart files only for formatting
|
||||||
|
|
||||||
|
- [ ] **Step 1: Format changed sources**
|
||||||
|
|
||||||
|
Run: `dart format lib/services/password_policy.dart lib/services/auth_service.dart lib/customWidget/custom_alert_dialog.dart lib/pages/mine/force_change_password_dialog.dart lib/pages/mine/mine_set_pwd_page.dart lib/pages/main_tab.dart lib/pages/user/login_page.dart lib/pages/user/choose_userFirm_page.dart test/services/password_policy_test.dart test/pages/mine/force_change_password_dialog_test.dart test/pages/main_tab_forced_password_change_test.dart`
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run focused regression tests**
|
||||||
|
|
||||||
|
Run: `flutter test test/services/password_policy_test.dart test/pages/mine/force_change_password_dialog_test.dart test/pages/main_tab_forced_password_change_test.dart test/services/session_service_test.dart`
|
||||||
|
|
||||||
|
Expected: PASS with zero failures.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run static analysis**
|
||||||
|
|
||||||
|
Run: `flutter analyze lib/services/password_policy.dart lib/services/auth_service.dart lib/customWidget/custom_alert_dialog.dart lib/pages/mine/force_change_password_dialog.dart lib/pages/mine/mine_set_pwd_page.dart lib/pages/main_tab.dart lib/pages/user/login_page.dart lib/pages/user/choose_userFirm_page.dart`
|
||||||
|
|
||||||
|
Expected: exit code 0, or only diagnostics demonstrably unrelated to changed code.
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:qhd_prevention/pages/main_tab.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('requires a change only for the default password', () {
|
||||||
|
expect(MainPage.shouldForcePasswordChange('Cc@12345678'), isTrue);
|
||||||
|
expect(MainPage.shouldForcePasswordChange('new-password'), isFalse);
|
||||||
|
expect(MainPage.shouldForcePasswordChange(null), isFalse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:qhd_prevention/pages/mine/force_change_password_dialog.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('requires a valid matching password before updating', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
var updateCalls = 0;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder:
|
||||||
|
(context) => TextButton(
|
||||||
|
onPressed:
|
||||||
|
() => ForceChangePasswordDialog.show(
|
||||||
|
context,
|
||||||
|
oldPassword: 'Cc@12345678',
|
||||||
|
onUpdatePassword: (_, __) async {
|
||||||
|
updateCalls++;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: const Text('open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('请修改默认密码'), findsOneWidget);
|
||||||
|
final firstField = tester.widget<TextField>(find.byType(TextField).first);
|
||||||
|
final decoration = firstField.decoration!;
|
||||||
|
expect(decoration.border, isA<UnderlineInputBorder>());
|
||||||
|
expect(decoration.enabledBorder, isA<UnderlineInputBorder>());
|
||||||
|
expect(decoration.focusedBorder, isA<UnderlineInputBorder>());
|
||||||
|
|
||||||
|
await tester.binding.handlePopRoute();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('请修改默认密码'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField).at(1), 'Aa@12345');
|
||||||
|
await tester.enterText(find.byType(TextField).at(2), 'different');
|
||||||
|
await tester.tap(find.text('提交'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(updateCalls, 0);
|
||||||
|
expect(find.text('新密码和确认密码不一致'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('submits a valid new password and closes the dialog', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
String? submittedOldPassword;
|
||||||
|
String? submittedNewPassword;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Builder(
|
||||||
|
builder:
|
||||||
|
(context) => TextButton(
|
||||||
|
onPressed:
|
||||||
|
() => ForceChangePasswordDialog.show(
|
||||||
|
context,
|
||||||
|
oldPassword: 'Cc@12345678',
|
||||||
|
onUpdatePassword: (oldPassword, newPassword) async {
|
||||||
|
submittedOldPassword = oldPassword;
|
||||||
|
submittedNewPassword = newPassword;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
child: const Text('open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(find.byType(TextField).at(1), 'Aa@12345');
|
||||||
|
await tester.enterText(find.byType(TextField).at(2), 'Aa@12345');
|
||||||
|
await tester.tap(find.text('提交'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(submittedOldPassword, 'Cc@12345678');
|
||||||
|
expect(submittedNewPassword, 'Aa@12345');
|
||||||
|
expect(find.text('请修改默认密码'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:qhd_prevention/services/auth_service.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('stops login processing when the login API reports failure', () {
|
||||||
|
expect(
|
||||||
|
AuthService.isLoginResponseSuccessful({
|
||||||
|
'success': false,
|
||||||
|
'errCode': 'BIZ_ERROR',
|
||||||
|
'errMessage': '解密失败',
|
||||||
|
'data': null,
|
||||||
|
}),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:qhd_prevention/services/password_policy.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('recognizes the configured default password', () {
|
||||||
|
expect(PasswordPolicy.requiresChange('Cc@12345678'), isTrue);
|
||||||
|
expect(PasswordPolicy.requiresChange('Cc@12345679'), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts a valid password and rejects invalid character sets', () {
|
||||||
|
expect(PasswordPolicy.validate('Aa@12345'), isNull);
|
||||||
|
expect(PasswordPolicy.validate(r'Aa$12345'), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects passwords outside the 8 to 16 character range', () {
|
||||||
|
expect(PasswordPolicy.validate('Aa@1234'), isNotNull);
|
||||||
|
expect(PasswordPolicy.validate('Aa@12345678901234'), isNotNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:qhd_prevention/services/SessionService.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('preserves the nine-company device-management flag from login data', () {
|
||||||
|
final user = UserData.fromJson({'nineCompanyFlag': true});
|
||||||
|
|
||||||
|
expect(user.nineCompanyFlag, isTrue);
|
||||||
|
expect(user.toJson()['nineCompanyFlag'], isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue