feat(user): 사용자 자기정보 편집과 관리자 재설정 플로우를 연동
- lib/widgets/app_shell.dart에서 내 정보 다이얼로그를 추가하고 UserRepository.updateMe·비밀번호 변경 로직을 연결 - lib/features/masters/user/* 모듈에 phone·forcePasswordChange·passwordUpdatedAt 필드를 반영하고 reset-password/update-me API를 사용 - lib/core/validation/password_rules.dart을 신설해 비밀번호 정책 검증을 공통화하고 신규 위젯·테스트에서 재사용 - doc/stock_approval_system_api_v4.md 등 문서를 users 스펙 개편 내용으로 갱신하고 user_management_plan.md를 추가 - test/widgets/app_shell_test.dart 등에서 자기정보 수정·비밀번호 재설정 시나리오를 검증하고 기존 테스트를 보강
This commit is contained in:
57
test/core/validation/password_rules_test.dart
Normal file
57
test/core/validation/password_rules_test.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:superport_v2/core/validation/password_rules.dart';
|
||||
|
||||
void main() {
|
||||
group('PasswordRules', () {
|
||||
test('정책을 모두 만족하면 위반 목록이 비어 있다', () {
|
||||
const password = 'Aa1!abcd';
|
||||
|
||||
final result = PasswordRules.validate(password);
|
||||
|
||||
expect(result, isEmpty);
|
||||
expect(PasswordRules.isValid(password), isTrue);
|
||||
});
|
||||
|
||||
test('길이가 짧으면 tooShort 위반이 발생한다', () {
|
||||
final result = PasswordRules.validate('Aa1!');
|
||||
|
||||
expect(result, contains(PasswordRuleViolation.tooShort));
|
||||
expect(result, isNot(contains(PasswordRuleViolation.tooLong)));
|
||||
});
|
||||
|
||||
test('길이가 길면 tooLong 위반이 발생한다', () {
|
||||
const password =
|
||||
'Aa1!'
|
||||
'abcdefghijklmnopqrstu';
|
||||
|
||||
final result = PasswordRules.validate(password);
|
||||
|
||||
expect(result, contains(PasswordRuleViolation.tooLong));
|
||||
});
|
||||
|
||||
test('대문자가 없으면 missingUppercase 위반이 발생한다', () {
|
||||
final result = PasswordRules.validate('aa1!aaaa');
|
||||
|
||||
expect(result, contains(PasswordRuleViolation.missingUppercase));
|
||||
});
|
||||
|
||||
test('소문자가 없으면 missingLowercase 위반이 발생한다', () {
|
||||
final result = PasswordRules.validate('AA1!AAAA');
|
||||
|
||||
expect(result, contains(PasswordRuleViolation.missingLowercase));
|
||||
});
|
||||
|
||||
test('숫자가 없으면 missingDigit 위반이 발생한다', () {
|
||||
final result = PasswordRules.validate('AAa!aaaa');
|
||||
|
||||
expect(result, contains(PasswordRuleViolation.missingDigit));
|
||||
});
|
||||
|
||||
test('특수 문자가 없으면 missingSpecial 위반이 발생한다', () {
|
||||
final result = PasswordRules.validate('AAa1aaaa');
|
||||
|
||||
expect(result, contains(PasswordRuleViolation.missingSpecial));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -80,7 +80,7 @@ void main() {
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresAt: DateTime.now().add(const Duration(hours: 1)),
|
||||
user: const AuthenticatedUser(id: 1, name: '테스터'),
|
||||
user: const AuthenticatedUser(id: 1, name: '테스터', phone: null),
|
||||
permissions: const [],
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'package:superport_v2/core/network/api_client.dart';
|
||||
import 'package:superport_v2/features/masters/user/data/repositories/user_repository_remote.dart';
|
||||
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
|
||||
|
||||
class _MockApiClient extends Mock implements ApiClient {}
|
||||
|
||||
@@ -14,6 +15,9 @@ void main() {
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Options());
|
||||
registerFallbackValue(CancelToken());
|
||||
registerFallbackValue(
|
||||
Response<dynamic>(requestOptions: RequestOptions(path: '/fallback')),
|
||||
);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
@@ -35,13 +39,15 @@ void main() {
|
||||
'items': [
|
||||
{
|
||||
'id': 1,
|
||||
'employee_no': 'E-001',
|
||||
'employee_name': '홍길동',
|
||||
'employee_id': 'E-001',
|
||||
'name': '홍길동',
|
||||
'group': {'id': 2, 'group_name': '관리자'},
|
||||
'force_password_change': false,
|
||||
'password_updated_at': '2025-01-10T09:00:00Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
requestOptions: RequestOptions(path: '/api/v1/employees'),
|
||||
requestOptions: RequestOptions(path: '/api/v1/users'),
|
||||
statusCode: 200,
|
||||
),
|
||||
);
|
||||
@@ -59,7 +65,150 @@ void main() {
|
||||
final path = captured[0] as String;
|
||||
final query = captured[1] as Map<String, dynamic>;
|
||||
|
||||
expect(path, equals('/api/v1/employees'));
|
||||
expect(path, equals('/api/v1/users'));
|
||||
expect(query['include'], 'group');
|
||||
});
|
||||
|
||||
test('create 호출 시 employee_id 파라미터를 전달한다', () async {
|
||||
final response = Response<Map<String, dynamic>>(
|
||||
data: {
|
||||
'data': {
|
||||
'id': 10,
|
||||
'employee_id': 'E2025001',
|
||||
'name': '김승인',
|
||||
'email': 'approver@example.com',
|
||||
'phone': '+82-10-1111-2222',
|
||||
'group': {'id': 1, 'group_name': '관리자'},
|
||||
'force_password_change': true,
|
||||
'is_active': true,
|
||||
},
|
||||
},
|
||||
requestOptions: RequestOptions(path: '/api/v1/users'),
|
||||
statusCode: 201,
|
||||
);
|
||||
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
any(),
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer((_) async => response);
|
||||
when(
|
||||
() => apiClient.unwrapAsMap(response),
|
||||
).thenReturn(response.data!['data'] as Map<String, dynamic>);
|
||||
|
||||
final input = UserInput(
|
||||
employeeNo: 'E2025001',
|
||||
employeeName: '김승인',
|
||||
groupId: 1,
|
||||
email: 'approver@example.com',
|
||||
mobileNo: '+82-10-1111-2222',
|
||||
password: 'TempPass!1',
|
||||
);
|
||||
|
||||
final user = await repository.create(input);
|
||||
|
||||
expect(user.employeeNo, 'E2025001');
|
||||
expect(user.forcePasswordChange, isTrue);
|
||||
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/users',
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('updateMe 호출 시 /users/me 엔드포인트를 사용한다', () async {
|
||||
final response = Response<Map<String, dynamic>>(
|
||||
data: {
|
||||
'data': {
|
||||
'id': 7,
|
||||
'employee_id': 'E2025001',
|
||||
'name': '김승인',
|
||||
'email': 'approver@example.com',
|
||||
'phone': '+82-10-1111-2222',
|
||||
'force_password_change': false,
|
||||
},
|
||||
},
|
||||
requestOptions: RequestOptions(path: '/api/v1/users/me'),
|
||||
statusCode: 200,
|
||||
);
|
||||
|
||||
when(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
any(),
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer((_) async => response);
|
||||
when(
|
||||
() => apiClient.unwrapAsMap(response),
|
||||
).thenReturn(response.data!['data'] as Map<String, dynamic>);
|
||||
|
||||
final payload = UserProfileUpdateInput(
|
||||
email: 'approver@example.com',
|
||||
phone: '+82-10-1111-2222',
|
||||
password: 'NewPass!23',
|
||||
currentPassword: 'TempPass!1',
|
||||
);
|
||||
|
||||
final user = await repository.updateMe(payload);
|
||||
|
||||
expect(user.email, 'approver@example.com');
|
||||
verify(
|
||||
() => apiClient.patch<Map<String, dynamic>>(
|
||||
'/api/v1/users/me',
|
||||
data: payload.toPayload(),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('resetPassword 호출 시 /users/{id}/reset-password 엔드포인트를 사용한다', () async {
|
||||
final response = Response<Map<String, dynamic>>(
|
||||
data: {
|
||||
'data': {
|
||||
'id': 7,
|
||||
'employee_id': 'E2025001',
|
||||
'email': 'approver@example.com',
|
||||
'force_password_change': true,
|
||||
'password_updated_at': '2025-03-11T02:05:00Z',
|
||||
},
|
||||
},
|
||||
requestOptions: RequestOptions(path: '/api/v1/users/7/reset-password'),
|
||||
statusCode: 200,
|
||||
);
|
||||
|
||||
when(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
any(),
|
||||
data: any(named: 'data'),
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).thenAnswer((_) async => response);
|
||||
when(
|
||||
() => apiClient.unwrapAsMap(response),
|
||||
).thenReturn(response.data!['data'] as Map<String, dynamic>);
|
||||
|
||||
final user = await repository.resetPassword(7);
|
||||
|
||||
expect(user.forcePasswordChange, isTrue);
|
||||
expect(user.passwordUpdatedAt, isNotNull);
|
||||
verify(
|
||||
() => apiClient.post<Map<String, dynamic>>(
|
||||
'/api/v1/users/7/reset-password',
|
||||
data: null,
|
||||
options: any(named: 'options'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -269,5 +269,16 @@ void main() {
|
||||
expect(restored, isNotNull);
|
||||
verify(() => userRepository.restore(1)).called(1);
|
||||
});
|
||||
|
||||
test('resetPassword 성공', () async {
|
||||
when(
|
||||
() => userRepository.resetPassword(any()),
|
||||
).thenAnswer((_) async => sampleUser);
|
||||
|
||||
final result = await controller.resetPassword(1);
|
||||
|
||||
expect(result, isNotNull);
|
||||
verify(() => userRepository.resetPassword(1)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,7 +200,7 @@ void main() {
|
||||
|
||||
testWidgets('신규 등록 성공', (tester) async {
|
||||
final view = tester.view;
|
||||
view.physicalSize = const Size(1280, 800);
|
||||
view.physicalSize = const Size(1600, 900);
|
||||
view.devicePixelRatio = 1.0;
|
||||
addTearDown(() {
|
||||
view.resetPhysicalSize();
|
||||
@@ -260,13 +260,26 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialog = find.byType(Dialog);
|
||||
final editableTexts = find.descendant(
|
||||
of: dialog,
|
||||
matching: find.byType(EditableText),
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_employee')),
|
||||
'A010',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_name')),
|
||||
'신규 사용자',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_password')),
|
||||
'Aa1!abcd',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_email')),
|
||||
'new@superport.com',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_phone')),
|
||||
'010-1111-2222',
|
||||
);
|
||||
|
||||
await tester.enterText(editableTexts.at(0), 'A010');
|
||||
await tester.enterText(editableTexts.at(1), '신규 사용자');
|
||||
|
||||
final selectFinder = find.descendant(
|
||||
of: dialog,
|
||||
@@ -290,9 +303,149 @@ void main() {
|
||||
|
||||
expect(capturedInput, isNotNull);
|
||||
expect(capturedInput?.employeeNo, 'A010');
|
||||
expect(capturedInput?.password, 'Aa1!abcd');
|
||||
expect(capturedInput?.forcePasswordChange, isTrue);
|
||||
expect(capturedInput?.email, 'new@superport.com');
|
||||
expect(capturedInput?.mobileNo, '010-1111-2222');
|
||||
expect(find.byType(Dialog), findsNothing);
|
||||
expect(find.text('A010'), findsOneWidget);
|
||||
verify(() => userRepository.create(any())).called(1);
|
||||
});
|
||||
|
||||
testWidgets('비밀번호 정책을 위반하면 에러가 노출되고 생성이 중단된다', (tester) async {
|
||||
final view = tester.view;
|
||||
view.physicalSize = const Size(1280, 800);
|
||||
view.devicePixelRatio = 1.0;
|
||||
addTearDown(() {
|
||||
view.resetPhysicalSize();
|
||||
view.resetDevicePixelRatio();
|
||||
});
|
||||
|
||||
when(
|
||||
() => userRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
groupId: any(named: 'groupId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<UserAccount>(
|
||||
items: const [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const UserPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('신규 등록'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final dialog = find.byType(Dialog);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_employee')),
|
||||
'A011',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_name')),
|
||||
'정책 위반',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_password')),
|
||||
'abc',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_email')),
|
||||
'invalid@superport.com',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('user_form_phone')),
|
||||
'010-3333-4444',
|
||||
);
|
||||
|
||||
final selectFinder = find.descendant(
|
||||
of: dialog,
|
||||
matching: find.byType(ShadSelect<int?>),
|
||||
);
|
||||
final selectElement = tester.element(selectFinder);
|
||||
final renderBox = selectElement.renderObject as RenderBox;
|
||||
final globalCenter = renderBox.localToGlobal(
|
||||
renderBox.size.center(Offset.zero),
|
||||
);
|
||||
await tester.tapAt(globalCenter);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.tap(find.text('관리자', skipOffstage: false).first);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('등록'));
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('최소 8자 이상 입력해야 합니다.'), findsOneWidget);
|
||||
verifyNever(() => userRepository.create(any()));
|
||||
});
|
||||
|
||||
testWidgets('비밀번호 재설정 버튼을 통해 확인 후 API 호출', (tester) async {
|
||||
final view = tester.view;
|
||||
view.physicalSize = const Size(1280, 800);
|
||||
view.devicePixelRatio = 1.0;
|
||||
addTearDown(() {
|
||||
view.resetPhysicalSize();
|
||||
view.resetDevicePixelRatio();
|
||||
});
|
||||
|
||||
when(
|
||||
() => userRepository.list(
|
||||
page: any(named: 'page'),
|
||||
pageSize: any(named: 'pageSize'),
|
||||
query: any(named: 'query'),
|
||||
groupId: any(named: 'groupId'),
|
||||
isActive: any(named: 'isActive'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(_) async => PaginatedResult<UserAccount>(
|
||||
items: [
|
||||
UserAccount(
|
||||
id: 1,
|
||||
employeeNo: 'A001',
|
||||
employeeName: '홍길동',
|
||||
group: UserGroup(id: 1, groupName: '관리자'),
|
||||
),
|
||||
],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 1,
|
||||
),
|
||||
);
|
||||
when(() => userRepository.resetPassword(any())).thenAnswer(
|
||||
(_) async => UserAccount(
|
||||
id: 1,
|
||||
employeeNo: 'A001',
|
||||
employeeName: '홍길동',
|
||||
group: UserGroup(id: 1, groupName: '관리자'),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(_buildApp(const UserPage()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final resetFinder = find
|
||||
.widgetWithIcon(ShadButton, LucideIcons.refreshCcw)
|
||||
.first;
|
||||
final resetButton = tester.widget<ShadButton>(resetFinder);
|
||||
resetButton.onPressed?.call();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(Dialog), findsOneWidget);
|
||||
expect(find.text('재설정'), findsOneWidget);
|
||||
await tester.tap(find.text('재설정'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => userRepository.resetPassword(1)).called(1);
|
||||
expect(find.text('비밀번호 재설정'), findsNothing);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ AuthSession _buildSampleSession() {
|
||||
accessToken: 'access-token',
|
||||
refreshToken: 'refresh-token',
|
||||
expiresAt: null,
|
||||
user: AuthenticatedUser(id: 1, name: '테스터'),
|
||||
user: AuthenticatedUser(id: 1, name: '테스터', phone: null),
|
||||
permissions: [],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart' as lucide;
|
||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||
|
||||
import 'package:superport_v2/core/common/models/paginated_result.dart';
|
||||
import 'package:superport_v2/core/constants/app_sections.dart';
|
||||
import 'package:superport_v2/core/permissions/permission_manager.dart';
|
||||
import 'package:superport_v2/core/theme/superport_shad_theme.dart';
|
||||
@@ -16,6 +17,8 @@ import 'package:superport_v2/features/auth/domain/entities/auth_session.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/authenticated_user.dart';
|
||||
import 'package:superport_v2/features/auth/domain/entities/login_request.dart';
|
||||
import 'package:superport_v2/features/auth/domain/repositories/auth_repository.dart';
|
||||
import 'package:superport_v2/features/masters/user/domain/entities/user.dart';
|
||||
import 'package:superport_v2/features/masters/user/domain/repositories/user_repository.dart';
|
||||
import 'package:superport_v2/widgets/app_shell.dart';
|
||||
|
||||
void main() {
|
||||
@@ -27,6 +30,7 @@ void main() {
|
||||
final session = _buildSession();
|
||||
final authService = _createAuthService(session);
|
||||
GetIt.I.registerSingleton<AuthService>(authService);
|
||||
GetIt.I.registerSingleton<UserRepository>(_StubUserRepository());
|
||||
addTearDown(authService.dispose);
|
||||
await authService.login(
|
||||
const LoginRequest(identifier: 'user@example.com', password: 'secret'),
|
||||
@@ -37,10 +41,12 @@ void main() {
|
||||
await tester.tap(find.byIcon(lucide.LucideIcons.userRound));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('계정 정보'), findsOneWidget);
|
||||
expect(find.text('내 정보'), findsOneWidget);
|
||||
expect(find.text('김승인'), findsWidgets);
|
||||
expect(find.text('E2025001'), findsOneWidget);
|
||||
expect(find.text('물류팀'), findsOneWidget);
|
||||
expect(find.text('연락처 / 이메일 수정'), findsOneWidget);
|
||||
expect(find.text('비밀번호 변경'), findsOneWidget);
|
||||
expect(find.textContaining('/approvals'), findsOneWidget);
|
||||
});
|
||||
|
||||
@@ -48,6 +54,7 @@ void main() {
|
||||
final session = _buildSession();
|
||||
final authService = _createAuthService(session);
|
||||
GetIt.I.registerSingleton<AuthService>(authService);
|
||||
GetIt.I.registerSingleton<UserRepository>(_StubUserRepository());
|
||||
addTearDown(authService.dispose);
|
||||
await authService.login(
|
||||
const LoginRequest(identifier: 'user@example.com', password: 'secret'),
|
||||
@@ -66,6 +73,133 @@ void main() {
|
||||
expect(authService.session, isNull);
|
||||
expect(find.text('로그인 페이지'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('내 정보 다이얼로그에서 연락처/이메일 저장 시 updateMe 호출', (tester) async {
|
||||
final session = _buildSession();
|
||||
final authService = _createAuthService(session);
|
||||
final captured = <UserProfileUpdateInput>[];
|
||||
final repository = _StubUserRepository(onUpdateMe: (input) async {
|
||||
captured.add(input);
|
||||
return UserAccount(
|
||||
id: session.user.id,
|
||||
employeeNo: session.user.employeeNo ?? '',
|
||||
employeeName: session.user.name,
|
||||
email: input.email,
|
||||
mobileNo: input.phone,
|
||||
group: UserGroup(id: 1, groupName: '물류팀'),
|
||||
);
|
||||
});
|
||||
|
||||
GetIt.I.registerSingleton<AuthService>(authService);
|
||||
GetIt.I.registerSingleton<UserRepository>(repository);
|
||||
addTearDown(authService.dispose);
|
||||
await authService.login(
|
||||
const LoginRequest(identifier: 'user@example.com', password: 'secret'),
|
||||
);
|
||||
|
||||
final view = tester.view;
|
||||
view.physicalSize = const Size(1600, 900);
|
||||
view.devicePixelRatio = 1.0;
|
||||
addTearDown(() {
|
||||
view.resetPhysicalSize();
|
||||
view.resetDevicePixelRatio();
|
||||
});
|
||||
|
||||
await _pumpAppShell(tester);
|
||||
|
||||
await tester.tap(find.byIcon(lucide.LucideIcons.userRound));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('account_email_field')),
|
||||
'new@superport.com',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('account_phone_field')),
|
||||
'+82-10-9999-8888',
|
||||
);
|
||||
|
||||
await tester.tap(find.text('저장'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(captured, hasLength(1));
|
||||
final input = captured.single;
|
||||
expect(input.email, 'new@superport.com');
|
||||
expect(input.phone, '+82-10-9999-8888');
|
||||
expect(authService.session?.user.email, 'new@superport.com');
|
||||
expect(authService.session?.user.phone, '+82-10-9999-8888');
|
||||
});
|
||||
|
||||
testWidgets('비밀번호 변경 완료 시 강제 로그아웃 안내 후 세션 초기화', (tester) async {
|
||||
final session = _buildSession();
|
||||
final authService = _createAuthService(session);
|
||||
UserProfileUpdateInput? passwordInput;
|
||||
final repository = _StubUserRepository(onUpdateMe: (input) async {
|
||||
passwordInput = input;
|
||||
return UserAccount(
|
||||
id: session.user.id,
|
||||
employeeNo: session.user.employeeNo ?? '',
|
||||
employeeName: session.user.name,
|
||||
email: input.email ?? session.user.email,
|
||||
mobileNo: input.phone ?? session.user.phone,
|
||||
group: UserGroup(id: 1, groupName: '물류팀'),
|
||||
);
|
||||
});
|
||||
|
||||
GetIt.I.registerSingleton<AuthService>(authService);
|
||||
GetIt.I.registerSingleton<UserRepository>(repository);
|
||||
addTearDown(authService.dispose);
|
||||
await authService.login(
|
||||
const LoginRequest(identifier: 'user@example.com', password: 'secret'),
|
||||
);
|
||||
|
||||
final view = tester.view;
|
||||
view.physicalSize = const Size(1600, 900);
|
||||
view.devicePixelRatio = 1.0;
|
||||
addTearDown(() {
|
||||
view.resetPhysicalSize();
|
||||
view.resetDevicePixelRatio();
|
||||
});
|
||||
|
||||
await _pumpAppShell(tester);
|
||||
|
||||
await tester.tap(find.byIcon(lucide.LucideIcons.userRound));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final changeButton = find.text('비밀번호 변경');
|
||||
await tester.ensureVisible(changeButton);
|
||||
await tester.tap(changeButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('account_current_password')),
|
||||
'TempPass1!',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('account_new_password')),
|
||||
'Aa1!zzzz',
|
||||
);
|
||||
await tester.enterText(
|
||||
find.byKey(const ValueKey('account_confirm_password')),
|
||||
'Aa1!zzzz',
|
||||
);
|
||||
|
||||
await tester.tap(find.text('변경'));
|
||||
await tester.pump();
|
||||
|
||||
expect(passwordInput, isNotNull);
|
||||
expect(passwordInput!.currentPassword, 'TempPass1!');
|
||||
expect(passwordInput!.password, 'Aa1!zzzz');
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('비밀번호 변경 완료'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('확인'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(authService.session, isNull);
|
||||
expect(find.text('로그인 페이지'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pumpAppShell(WidgetTester tester) async {
|
||||
@@ -122,6 +256,7 @@ AuthSession _buildSession() {
|
||||
name: '김승인',
|
||||
employeeNo: 'E2025001',
|
||||
email: 'approver@example.com',
|
||||
phone: '+82-10-2222-1111',
|
||||
primaryGroupId: 3,
|
||||
primaryGroupName: '물류팀',
|
||||
),
|
||||
@@ -194,3 +329,53 @@ class _LoginPlaceholder extends StatelessWidget {
|
||||
return const Scaffold(body: Center(child: Text('로그인 페이지')));
|
||||
}
|
||||
}
|
||||
|
||||
class _StubUserRepository implements UserRepository {
|
||||
_StubUserRepository({this.onUpdateMe});
|
||||
|
||||
final Future<UserAccount> Function(UserProfileUpdateInput input)? onUpdateMe;
|
||||
|
||||
@override
|
||||
Future<UserAccount> create(UserInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PaginatedResult<UserAccount>> list({
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
String? query,
|
||||
int? groupId,
|
||||
bool? isActive,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserAccount> resetPassword(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserAccount> restore(int id) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserAccount> update(int id, UserInput input) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UserAccount> updateMe(UserProfileUpdateInput input) {
|
||||
if (onUpdateMe != null) {
|
||||
return onUpdateMe!(input);
|
||||
}
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user