import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; 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'; import 'package:superport_v2/core/theme/theme_controller.dart'; import 'package:superport_v2/core/services/token_storage.dart'; import 'package:superport_v2/features/auth/application/auth_service.dart'; import 'package:superport_v2/features/auth/domain/entities/auth_permission.dart'; 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() { setUp(() { GetIt.I.reset(); }); testWidgets('계정 버튼을 누르면 계정 정보 다이얼로그가 표시된다', (tester) async { final session = _buildSession(); final authService = _createAuthService(session); GetIt.I.registerSingleton(authService); GetIt.I.registerSingleton(_StubUserRepository()); addTearDown(authService.dispose); await authService.login( const LoginRequest(identifier: 'user@example.com', password: 'secret'), ); await _pumpAppShell(tester); await tester.tap(find.byIcon(lucide.LucideIcons.userRound)); await tester.pumpAndSettle(); 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); }); testWidgets('다이얼로그에서 로그아웃을 누르면 세션이 초기화되고 로그인 화면으로 이동한다', (tester) async { final session = _buildSession(); final authService = _createAuthService(session); GetIt.I.registerSingleton(authService); GetIt.I.registerSingleton(_StubUserRepository()); addTearDown(authService.dispose); await authService.login( const LoginRequest(identifier: 'user@example.com', password: 'secret'), ); await _pumpAppShell(tester); expect(authService.session, isNotNull); await tester.tap(find.byIcon(lucide.LucideIcons.userRound)); await tester.pumpAndSettle(); await tester.tap(find.text('로그아웃')); await tester.pumpAndSettle(); expect(authService.session, isNull); expect(find.text('로그인 페이지'), findsOneWidget); }); testWidgets('내 정보 다이얼로그에서 연락처/이메일 저장 시 updateMe 호출', (tester) async { final session = _buildSession(); final authService = _createAuthService(session); final captured = []; 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); GetIt.I.registerSingleton(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); GetIt.I.registerSingleton(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 _pumpAppShell(WidgetTester tester) async { final themeController = ThemeController(); final permissionManager = PermissionManager(); final router = GoRouter( initialLocation: dashboardRoutePath, routes: [ GoRoute( path: loginRoutePath, builder: (context, state) => const _LoginPlaceholder(), ), ShellRoute( builder: (context, state, child) => AppShell(currentLocation: state.uri.toString(), child: child), routes: [ GoRoute( path: dashboardRoutePath, builder: (context, state) => const _DashboardPlaceholder(), ), ], ), ], ); addTearDown(themeController.dispose); addTearDown(permissionManager.dispose); addTearDown(router.dispose); await tester.pumpWidget( PermissionScope( manager: permissionManager, child: ThemeControllerScope( controller: themeController, child: ShadApp.router( routerConfig: router, theme: SuperportShadTheme.light(), darkTheme: SuperportShadTheme.dark(), debugShowCheckedModeBanner: false, ), ), ), ); await tester.pumpAndSettle(); } AuthSession _buildSession() { return AuthSession( accessToken: 'access-token', refreshToken: 'refresh-token', expiresAt: DateTime.parse('2025-10-21T09:00:00Z'), user: const AuthenticatedUser( id: 7, name: '김승인', employeeNo: 'E2025001', email: 'approver@example.com', phone: '+82-10-2222-1111', primaryGroupId: 3, primaryGroupName: '물류팀', ), permissions: const [ AuthPermission(resource: '/dashboard', actions: ['read']), AuthPermission(resource: '/approvals', actions: ['read', 'update']), ], ); } AuthService _createAuthService(AuthSession session) { final repository = _FakeAuthRepository(session); final tokenStorage = _MemoryTokenStorage(); return AuthService(repository: repository, tokenStorage: tokenStorage); } class _FakeAuthRepository implements AuthRepository { _FakeAuthRepository(this.session); final AuthSession session; @override Future login(LoginRequest request) async => session; @override Future refresh(String refreshToken) async => session; } class _MemoryTokenStorage implements TokenStorage { String? _access; String? _refresh; @override Future clear() async { _access = null; _refresh = null; } @override Future readAccessToken() async => _access; @override Future readRefreshToken() async => _refresh; @override Future writeAccessToken(String? token) async { _access = token; } @override Future writeRefreshToken(String? token) async { _refresh = token; } } class _DashboardPlaceholder extends StatelessWidget { const _DashboardPlaceholder(); @override Widget build(BuildContext context) { return const Scaffold(body: Center(child: Text('대시보드 본문'))); } } class _LoginPlaceholder extends StatelessWidget { const _LoginPlaceholder(); @override Widget build(BuildContext context) { return const Scaffold(body: Center(child: Text('로그인 페이지'))); } } class _StubUserRepository implements UserRepository { _StubUserRepository({this.onUpdateMe}); final Future Function(UserProfileUpdateInput input)? onUpdateMe; @override Future create(UserInput input) { throw UnimplementedError(); } @override Future delete(int id) { throw UnimplementedError(); } @override Future> list({ int page = 1, int pageSize = 20, String? query, int? groupId, bool? isActive, }) { throw UnimplementedError(); } @override Future resetPassword(int id) { throw UnimplementedError(); } @override Future restore(int id) { throw UnimplementedError(); } @override Future update(int id, UserInput input) { throw UnimplementedError(); } @override Future updateMe(UserProfileUpdateInput input) { if (onUpdateMe != null) { return onUpdateMe!(input); } throw UnimplementedError(); } }