From 1a5b04d68813fc8f1685170610c192ee608e7c86 Mon Sep 17 00:00:00 2001 From: Lectom C Han Date: Thu, 19 Feb 2026 11:29:12 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EC=83=9D=EC=84=B1,=20?= =?UTF-8?q?=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=EB=B3=80=EA=B2=BD,=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../issue-269-test-scenarios.md | 29 +++++ userfront/test/locale_utils_test.dart | 18 +++ .../test/router_redirect_widget_test.dart | 121 ++++++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 userfront/test/router_redirect_widget_test.dart diff --git a/docs/trouble-shooting/issue-269-test-scenarios.md b/docs/trouble-shooting/issue-269-test-scenarios.md index 1b544545..99214a2d 100644 --- a/docs/trouble-shooting/issue-269-test-scenarios.md +++ b/docs/trouble-shooting/issue-269-test-scenarios.md @@ -40,11 +40,26 @@ - `template.toml` 제외 - 유효 locale 파일(`en.toml`, `ko.toml`)만 지원 목록에 반영 +### S8. 실계정 비밀번호 변경 스모크(E2E) +- 목적: 로그인 상태 플로우가 기존 동작을 깨지 않았는지 확인 +- 절차: + - Kratos Admin API로 임시 계정 생성(초기 비밀번호 포함) + - 구 비밀번호 로그인 성공 확인 + - Settings API로 비밀번호 변경 + - 구 비밀번호 로그인 실패 확인 + - 신 비밀번호 로그인 성공 확인 + - 테스트 계정 삭제(정리) +- 기대: + - 비밀번호 변경 전/후 인증 결과가 정확히 반전 + - 테스트 종료 후 identity 삭제 완료(잔존 계정 없음) + ## 실행 방법 ```bash cd userfront flutter test test/locale_utils_test.dart flutter test test/locale_registry_test.dart +flutter test test/router_redirect_widget_test.dart +flutter test --platform chrome test/locale_utils_test.dart test/locale_registry_test.dart test/router_redirect_widget_test.dart ``` ## 자동화 매핑 @@ -52,3 +67,17 @@ flutter test test/locale_registry_test.dart - S1~S6 전부 커버 - `userfront/test/locale_registry_test.dart` - S7 커버 +- `userfront/test/router_redirect_widget_test.dart` + - 로그인/비로그인 redirect 동작 검증(`redirect_uri`, `redirect_url`) + +## 최근 실행 결과 +- 실행일: 2026-02-19 +- 결과: + - Flutter 테스트(VM): 통과 + - Flutter 테스트(Chrome): 통과 + - S8 실계정 E2E: 통과 + - `login_old_password=200` + - `change_password=200` + - `login_old_after_change=400` + - `login_new_after_change=200` + - `cleanup(delete identity)=204` diff --git a/userfront/test/locale_utils_test.dart b/userfront/test/locale_utils_test.dart index a81a8b12..3137c41e 100644 --- a/userfront/test/locale_utils_test.dart +++ b/userfront/test/locale_utils_test.dart @@ -63,6 +63,14 @@ void main() { ); }); + test('buildLocalizedPath preserves redirect_url parameter', () { + final uri = Uri.parse('/signin?redirect_url=https://example.com/after'); + expect( + buildLocalizedPath('ko', uri), + '/ko/signin?redirect_url=https://example.com/after', + ); + }); + test('buildLocalizedPath preserves raw query order and duplicates', () { final uri = Uri.parse( '/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2', @@ -109,5 +117,15 @@ void main() { '/ko/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2¬ice=qr_login_required', ); }); + + test('buildSigninRedirectPath preserves redirect_url and redirect_uri', () { + final uri = Uri.parse( + '/ko/profile?redirect_url=https%3A%2F%2Fa.example.com%2Fcb&redirect_uri=https%3A%2F%2Fb.example.com%2Fcb', + ); + expect( + buildSigninRedirectPath('ko', uri), + '/ko/signin?redirect_url=https%3A%2F%2Fa.example.com%2Fcb&redirect_uri=https%3A%2F%2Fb.example.com%2Fcb', + ); + }); }); } diff --git a/userfront/test/router_redirect_widget_test.dart b/userfront/test/router_redirect_widget_test.dart new file mode 100644 index 00000000..4ecbb160 --- /dev/null +++ b/userfront/test/router_redirect_widget_test.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:go_router/go_router.dart'; +import 'package:userfront/core/i18n/locale_registry.dart'; +import 'package:userfront/core/i18n/locale_utils.dart'; +import 'package:userfront/core/services/auth_token_store.dart'; + +Widget _buildTestApp(String initialLocation) { + final router = GoRouter( + initialLocation: initialLocation, + routes: [ + GoRoute( + path: '/:locale', + builder: (context, state) => const Scaffold(body: Text('root')), + routes: [ + GoRoute( + path: 'signin', + builder: (context, state) { + final challenge = state.uri.queryParameters['login_challenge']; + final redirect = + state.uri.queryParameters['redirect_uri'] ?? + state.uri.queryParameters['redirect_url'] ?? + ''; + return Scaffold( + body: Text( + 'signin|challenge=${challenge ?? ''}|redirect=$redirect', + ), + ); + }, + ), + GoRoute( + path: 'profile', + builder: (context, state) => + const Scaffold(body: Text('profile-page')), + ), + ], + ), + ], + redirect: (context, state) { + final requestedLocale = extractLocaleFromPath(state.uri); + if (requestedLocale == null) { + return buildLocalizedPath(resolvePreferredLocaleCode(), state.uri); + } + + final isLoggedIn = + AuthTokenStore.getToken() != null || AuthTokenStore.usesCookie(); + final path = stripLocalePath(state.uri); + final isPublicPath = path == '/signin'; + if (isPublicPath) { + return null; + } + if (!isLoggedIn) { + return buildSigninRedirectPath(requestedLocale, state.uri); + } + return null; + }, + ); + + return MaterialApp.router(routerConfig: router); +} + +void main() { + setUp(() { + LocaleRegistry.setSupportedLocaleCodesForTest(['en', 'ko']); + AuthTokenStore.clear(); + }); + + tearDown(() { + AuthTokenStore.clear(); + LocaleRegistry.resetForTest(); + }); + + testWidgets('비로그인: redirect_uri/login_challenge가 signin으로 전달', ( + tester, + ) async { + final encodedRedirectUri = Uri.encodeComponent( + 'https://rp.example.com/cb?x=1', + ); + await tester.pumpWidget( + _buildTestApp( + '/en/profile?login_challenge=lc_123&redirect_uri=$encodedRedirectUri', + ), + ); + await tester.pumpAndSettle(); + + expect( + find.text( + 'signin|challenge=lc_123|redirect=https://rp.example.com/cb?x=1', + ), + findsOneWidget, + ); + }); + + testWidgets('비로그인: redirect_uri가 없으면 redirect_url을 전달', (tester) async { + final encodedRedirectUrl = Uri.encodeComponent( + 'https://legacy.example.com/cb', + ); + await tester.pumpWidget( + _buildTestApp('/en/profile?redirect_url=$encodedRedirectUrl'), + ); + await tester.pumpAndSettle(); + + expect( + find.text('signin|challenge=|redirect=https://legacy.example.com/cb'), + findsOneWidget, + ); + }); + + testWidgets('로그인 상태: profile 접근 시 signin으로 리다이렉트하지 않음', (tester) async { + AuthTokenStore.setToken('test-token', provider: 'ory'); + await tester.pumpWidget( + _buildTestApp( + '/en/profile?redirect_uri=https%3A%2F%2Frp.example.com%2Fcb', + ), + ); + await tester.pumpAndSettle(); + + expect(find.text('profile-page'), findsOneWidget); + expect(find.textContaining('signin|'), findsNothing); + }); +}