forked from baron/baron-sso
userfront 로그인 후 /dashboard로 이동하게 변경
This commit is contained in:
40
userfront/test/cookie_session_policy_test.dart
Normal file
40
userfront/test/cookie_session_policy_test.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/features/auth/domain/cookie_session_policy.dart';
|
||||
|
||||
void main() {
|
||||
group('cookie_session_policy', () {
|
||||
test('토큰이 없고 login_challenge도 없으면 cookie 승격 허용', () {
|
||||
expect(
|
||||
shouldPromoteCookieSession(currentToken: null, loginChallenge: null),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('토큰이 이미 있으면 일반 로그인에서 cookie 승격 차단', () {
|
||||
expect(
|
||||
shouldPromoteCookieSession(
|
||||
currentToken: 'existing-token',
|
||||
loginChallenge: null,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('OIDC login_challenge가 있으면 token 존재 시에도 cookie 승격 허용', () {
|
||||
expect(
|
||||
shouldPromoteCookieSession(
|
||||
currentToken: 'existing-token',
|
||||
loginChallenge: 'lc_123',
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('공백 토큰은 유효 토큰으로 간주하지 않음', () {
|
||||
expect(
|
||||
shouldPromoteCookieSession(currentToken: ' ', loginChallenge: null),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
50
userfront/test/dashboard_screen_smoke_test.dart
Normal file
50
userfront/test/dashboard_screen_smoke_test.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/core/services/auth_token_store.dart';
|
||||
import 'package:userfront/features/dashboard/presentation/dashboard_screen.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
AuthTokenStore.clear();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
AuthTokenStore.clear();
|
||||
});
|
||||
|
||||
testWidgets('대시보드는 로그인 토큰이 있으면 크래시 없이 기본 프레임을 렌더링한다', (tester) async {
|
||||
final recordedErrors = <FlutterErrorDetails>[];
|
||||
final previousOnError = FlutterError.onError;
|
||||
FlutterError.onError = (details) {
|
||||
final text = details.exceptionAsString();
|
||||
if (text.contains('A RenderFlex overflowed')) {
|
||||
return;
|
||||
}
|
||||
recordedErrors.add(details);
|
||||
};
|
||||
addTearDown(() {
|
||||
FlutterError.onError = previousOnError;
|
||||
});
|
||||
|
||||
tester.view.devicePixelRatio = 1.0;
|
||||
tester.view.physicalSize = const Size(1920, 1080);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
AuthTokenStore.setToken('smoke-token', provider: 'ory');
|
||||
|
||||
await tester.pumpWidget(
|
||||
const ProviderScope(child: MaterialApp(home: DashboardScreen())),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(Scaffold), findsOneWidget);
|
||||
final hasNullCheckCrash = recordedErrors.any(
|
||||
(error) => error.exceptionAsString().contains(
|
||||
'Null check operator used on a null value',
|
||||
),
|
||||
);
|
||||
expect(hasNullCheckCrash, isFalse);
|
||||
});
|
||||
}
|
||||
@@ -127,5 +127,32 @@ void main() {
|
||||
'/ko/signin?redirect_url=https%3A%2F%2Fa.example.com%2Fcb&redirect_uri=https%3A%2F%2Fb.example.com%2Fcb',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildLocalizedHomePath keeps locale from uri', () {
|
||||
expect(buildLocalizedHomePath(Uri.parse('/ko/signin')), '/ko/dashboard');
|
||||
expect(buildLocalizedHomePath(Uri.parse('/en/profile')), '/en/dashboard');
|
||||
});
|
||||
|
||||
test('buildLocalizedHomePath falls back to preferred locale', () {
|
||||
expect(
|
||||
buildLocalizedHomePath(Uri.parse('/signin'), preferredLocaleCode: 'ko'),
|
||||
'/ko/dashboard',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildLocalizedSigninPath keeps locale from uri', () {
|
||||
expect(buildLocalizedSigninPath(Uri.parse('/ko')), '/ko/signin');
|
||||
expect(buildLocalizedSigninPath(Uri.parse('/en/profile')), '/en/signin');
|
||||
});
|
||||
|
||||
test('buildLocalizedSigninPath falls back to preferred locale', () {
|
||||
expect(
|
||||
buildLocalizedSigninPath(
|
||||
Uri.parse('/profile'),
|
||||
preferredLocaleCode: 'ko',
|
||||
),
|
||||
'/ko/signin',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
94
userfront/test/login_navigation_race_test.dart
Normal file
94
userfront/test/login_navigation_race_test.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
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';
|
||||
|
||||
class _AuthRefreshNotifier extends ChangeNotifier {
|
||||
void refresh() => notifyListeners();
|
||||
}
|
||||
|
||||
Widget _buildRaceTestApp(_AuthRefreshNotifier notifier) {
|
||||
final router = GoRouter(
|
||||
initialLocation: '/ko/signin',
|
||||
refreshListenable: notifier,
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:locale',
|
||||
builder: (context, state) => const Scaffold(body: Text('locale-root')),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'dashboard',
|
||||
builder: (context, state) => const Scaffold(body: Text('home')),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'signin',
|
||||
builder: (context, state) {
|
||||
return Scaffold(
|
||||
body: Center(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
AuthTokenStore.setToken('race-token', provider: 'ory');
|
||||
notifier.refresh();
|
||||
context.go('/ko/dashboard');
|
||||
},
|
||||
child: const Text('login'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
redirect: (context, state) {
|
||||
final requestedLocale = extractLocaleFromPath(state.uri);
|
||||
if (requestedLocale == null) {
|
||||
return buildLocalizedPath(resolvePreferredLocaleCode(), state.uri);
|
||||
}
|
||||
|
||||
final token = AuthTokenStore.getToken();
|
||||
final isLoggedIn =
|
||||
(token != null && token.isNotEmpty) || AuthTokenStore.usesCookie();
|
||||
final path = stripLocalePath(state.uri);
|
||||
if (path == '/signin') {
|
||||
return null;
|
||||
}
|
||||
if (!isLoggedIn) {
|
||||
return buildSigninRedirectPath(requestedLocale, state.uri);
|
||||
}
|
||||
if (path == '/') {
|
||||
return '/$requestedLocale/dashboard';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
return MaterialApp.router(routerConfig: router);
|
||||
}
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
LocaleRegistry.setSupportedLocaleCodesForTest(['en', 'ko']);
|
||||
AuthTokenStore.clear();
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
AuthTokenStore.clear();
|
||||
LocaleRegistry.resetForTest();
|
||||
});
|
||||
|
||||
testWidgets('로그인 성공 이벤트(notify + go) 동시 호출 시 홈으로 안정적으로 이동', (tester) async {
|
||||
final notifier = _AuthRefreshNotifier();
|
||||
await tester.pumpWidget(_buildRaceTestApp(notifier));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('login'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('login'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('home'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
63
userfront/test/null_check_recovery_test.dart
Normal file
63
userfront/test/null_check_recovery_test.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/core/i18n/locale_registry.dart';
|
||||
import 'package:userfront/core/services/null_check_recovery.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
LocaleRegistry.setSupportedLocaleCodesForTest(['en', 'ko']);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
LocaleRegistry.resetForTest();
|
||||
});
|
||||
|
||||
test('Null check 오류 + 루트(/)면 선호 로케일 signin으로 복구', () {
|
||||
final target = computeNullCheckRecoveryTarget(
|
||||
exception: Exception('Null check operator used on a null value'),
|
||||
uri: Uri.parse('https://sss.hmac.kr/'),
|
||||
preferredLocaleCode: 'ko',
|
||||
);
|
||||
|
||||
expect(target, '/ko/signin');
|
||||
});
|
||||
|
||||
test('Null check 오류 + /ko면 /ko/signin으로 복구', () {
|
||||
final target = computeNullCheckRecoveryTarget(
|
||||
exception: Exception('Null check operator used on a null value'),
|
||||
uri: Uri.parse('https://sss.hmac.kr/ko'),
|
||||
preferredLocaleCode: 'en',
|
||||
);
|
||||
|
||||
expect(target, '/ko/signin');
|
||||
});
|
||||
|
||||
test('이미 /ko/signin이면 복구 이동하지 않음', () {
|
||||
final target = computeNullCheckRecoveryTarget(
|
||||
exception: Exception('Null check operator used on a null value'),
|
||||
uri: Uri.parse('https://sss.hmac.kr/ko/signin'),
|
||||
preferredLocaleCode: 'ko',
|
||||
);
|
||||
|
||||
expect(target, isNull);
|
||||
});
|
||||
|
||||
test('Null check 오류여도 /ko/profile에서는 복구 이동하지 않음', () {
|
||||
final target = computeNullCheckRecoveryTarget(
|
||||
exception: Exception('Null check operator used on a null value'),
|
||||
uri: Uri.parse('https://sss.hmac.kr/ko/profile'),
|
||||
preferredLocaleCode: 'ko',
|
||||
);
|
||||
|
||||
expect(target, isNull);
|
||||
});
|
||||
|
||||
test('다른 오류 메시지면 복구 이동하지 않음', () {
|
||||
final target = computeNullCheckRecoveryTarget(
|
||||
exception: Exception('Some other error'),
|
||||
uri: Uri.parse('https://sss.hmac.kr/ko'),
|
||||
preferredLocaleCode: 'ko',
|
||||
);
|
||||
|
||||
expect(target, isNull);
|
||||
});
|
||||
}
|
||||
@@ -11,8 +11,28 @@ Widget _buildTestApp(String initialLocation) {
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/:locale',
|
||||
builder: (context, state) => const Scaffold(body: Text('root')),
|
||||
redirect: (context, state) {
|
||||
if (state.uri.pathSegments.length != 1) {
|
||||
return null;
|
||||
}
|
||||
final localeCode = normalizeLocaleCode(
|
||||
state.pathParameters['locale'],
|
||||
);
|
||||
final token = AuthTokenStore.getToken();
|
||||
final isLoggedIn =
|
||||
(token != null && token.isNotEmpty) ||
|
||||
AuthTokenStore.usesCookie();
|
||||
if (!isLoggedIn) {
|
||||
return buildSigninRedirectPath(localeCode, state.uri);
|
||||
}
|
||||
return '/$localeCode/dashboard';
|
||||
},
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'dashboard',
|
||||
builder: (context, state) =>
|
||||
const Scaffold(body: Text('dashboard-page')),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'signin',
|
||||
builder: (context, state) {
|
||||
@@ -57,8 +77,9 @@ Widget _buildTestApp(String initialLocation) {
|
||||
return buildLocalizedPath(resolvePreferredLocaleCode(), state.uri);
|
||||
}
|
||||
|
||||
final token = AuthTokenStore.getToken();
|
||||
final isLoggedIn =
|
||||
AuthTokenStore.getToken() != null || AuthTokenStore.usesCookie();
|
||||
(token != null && token.isNotEmpty) || AuthTokenStore.usesCookie();
|
||||
final path = stripLocalePath(state.uri);
|
||||
final isPublicPath = path == '/signin' || path == '/login';
|
||||
if (isPublicPath) {
|
||||
@@ -85,6 +106,25 @@ void main() {
|
||||
LocaleRegistry.resetForTest();
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'루트 경로: /{locale} 로 접근 시 /{locale}/signin 으로 리다이렉트되어야 한다 (버그: 화면 렌더링 안됨)',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(_buildTestApp('/ko'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('signin|'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('로그인 상태에서 /{locale} 접근 시 dashboard로 이동', (tester) async {
|
||||
AuthTokenStore.setToken('root-token', provider: 'ory');
|
||||
await tester.pumpWidget(_buildTestApp('/ko'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('dashboard-page'), findsOneWidget);
|
||||
expect(find.textContaining('signin|'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('/login: login_challenge와 redirect_uri를 전달', (tester) async {
|
||||
final encodedRedirectUri = Uri.encodeComponent(
|
||||
'https://rp.example.com/callback?x=1',
|
||||
@@ -153,6 +193,15 @@ void main() {
|
||||
expect(find.textContaining('signin|'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('빈 토큰은 로그인으로 간주하지 않고 signin으로 리다이렉트', (tester) async {
|
||||
AuthTokenStore.setToken('', provider: 'ory');
|
||||
await tester.pumpWidget(_buildTestApp('/ko/profile'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.textContaining('signin|'), findsOneWidget);
|
||||
expect(find.text('profile-page'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('로그인 후 같은 브라우저 새 창/팝업에서도 세션이 유지된다', (tester) async {
|
||||
await tester.pumpWidget(_buildTestApp('/en/signin'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Reference in New Issue
Block a user