forked from baron/baron-sso
#269 진행. 리다이렉트 등 파라미터 전체 전달
This commit is contained in:
51
docs/trouble-shooting/issue-269-locale-query-loss.md
Normal file
51
docs/trouble-shooting/issue-269-locale-query-loss.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Issue #269 해결 기록: `/{locale}/` 도입 후 query parameter 유실
|
||||
|
||||
## 개요
|
||||
- 대상 이슈: `#269`
|
||||
- 증상: locale 보정 또는 비로그인 리다이렉트 과정에서 GET query parameter가 유실되거나 형태가 변형됨
|
||||
- 영향: OIDC 로그인 연계 파라미터(`login_challenge`, `redirect_uri`, `notice` 등) 전달 실패 가능
|
||||
|
||||
## 원인
|
||||
1. 비로그인 리다이렉트 시 `login_challenge`만 선택 보존하고 나머지 query를 폐기
|
||||
2. locale 경로 재작성 시 `uri.queryParameters` 기반 재직렬화로 원본 query 문자열(중복 key, 순서, 인코딩) 보존 실패
|
||||
3. `head.length == 2` 휴리스틱으로 locale이 아닌 2글자 경로 prefix까지 locale로 오인 가능
|
||||
|
||||
## 수정 사항
|
||||
|
||||
### 1) 비로그인 리다이렉트에서 raw query 전체 보존
|
||||
- 파일: `userfront/lib/main.dart`
|
||||
- 변경: `state.uri.query`를 그대로 `/[locale]/signin`에 연결
|
||||
|
||||
```dart
|
||||
final rawQuery = state.uri.query;
|
||||
if (rawQuery.isNotEmpty) {
|
||||
return '/$locale/signin?$rawQuery';
|
||||
}
|
||||
return '/$locale/signin';
|
||||
```
|
||||
|
||||
### 2) locale 경로 재작성 시 raw query/fragment 보존
|
||||
- 파일: `userfront/lib/core/i18n/locale_utils.dart`
|
||||
- 변경: `queryParameters` 재직렬화 제거, `uri.query`/`uri.fragment` 원문 유지
|
||||
|
||||
```dart
|
||||
final queryPart = uri.hasQuery ? '?${uri.query}' : '';
|
||||
final fragmentPart = uri.fragment.isNotEmpty ? '#${uri.fragment}' : '';
|
||||
return '$path$queryPart$fragmentPart';
|
||||
```
|
||||
|
||||
### 3) locale 판별 조건 엄격화
|
||||
- 파일: `userfront/lib/core/i18n/locale_utils.dart`
|
||||
- 변경: `head.length == 2` 휴리스틱 제거, `supportedLocaleCodes.contains(head)`만 허용
|
||||
|
||||
## 테스트 보강
|
||||
- 파일: `userfront/test/locale_utils_test.dart`
|
||||
- 추가/변경:
|
||||
- raw query 순서 및 중복 key(`a=1&a=2`) 보존
|
||||
- fragment 보존
|
||||
- unknown 2-letter prefix(`zz`)를 locale로 제거하지 않음
|
||||
|
||||
## 기대 결과
|
||||
- `/signin?redirect_uri=...¬ice=...` -> locale 보정 후 query 100% 유지
|
||||
- 비로그인 보호 경로 -> `/[locale]/signin` 이동 시 기존 query 유지
|
||||
- 인코딩된 nested `redirect_uri`, 중복 query key, fragment 보존
|
||||
54
docs/trouble-shooting/issue-269-test-scenarios.md
Normal file
54
docs/trouble-shooting/issue-269-test-scenarios.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Issue #269 테스트 시나리오
|
||||
|
||||
## 목적
|
||||
`/{locale}/` 라우팅 도입 이후 query parameter 유실 회귀를 방지합니다.
|
||||
|
||||
## 범위
|
||||
- UserFront locale 경로 보정 (`buildLocalizedPath`)
|
||||
- 비로그인 redirect 경로 생성 (`buildSigninRedirectPath`)
|
||||
- locale 지원 목록 동기화 (`assets/translations/*.toml` -> `LocaleRegistry`)
|
||||
|
||||
## 테스트 시나리오
|
||||
|
||||
### S1. locale 보정 시 기본 query 보존
|
||||
- 입력: `/signin?redirect_uri=https://example.com`
|
||||
- 기대: `/ko/signin?redirect_uri=https://example.com`
|
||||
|
||||
### S2. locale 보정 시 raw query 순서/중복 key 보존
|
||||
- 입력: `/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2`
|
||||
- 기대: `/ko/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2`
|
||||
|
||||
### S3. locale 보정 시 fragment 보존
|
||||
- 입력: `/signin?notice=qr_login_required#auth`
|
||||
- 기대: `/ko/signin?notice=qr_login_required#auth`
|
||||
|
||||
### S4. unknown 2-letter prefix 오인 제거
|
||||
- 입력: `/zz/signin`
|
||||
- 기대: `/ko/zz/signin`
|
||||
|
||||
### S5. 비로그인 redirect에서 query 없음
|
||||
- 입력: locale=`ko`, uri=`/ko/profile`
|
||||
- 기대: `/ko/signin`
|
||||
|
||||
### S6. 비로그인 redirect에서 query 전체 보존
|
||||
- 입력: locale=`ko`, uri=`/ko/profile?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2¬ice=qr_login_required`
|
||||
- 기대: `/ko/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2¬ice=qr_login_required`
|
||||
|
||||
### S7. locale 목록 하드코딩 제거 검증
|
||||
- 입력: asset 목록 (`assets/translations/en.toml`, `assets/translations/ko.toml`, `assets/translations/template.toml`, 기타 invalid 파일)
|
||||
- 기대:
|
||||
- `template.toml` 제외
|
||||
- 유효 locale 파일(`en.toml`, `ko.toml`)만 지원 목록에 반영
|
||||
|
||||
## 실행 방법
|
||||
```bash
|
||||
cd userfront
|
||||
flutter test test/locale_utils_test.dart
|
||||
flutter test test/locale_registry_test.dart
|
||||
```
|
||||
|
||||
## 자동화 매핑
|
||||
- `userfront/test/locale_utils_test.dart`
|
||||
- S1~S6 전부 커버
|
||||
- `userfront/test/locale_registry_test.dart`
|
||||
- S7 커버
|
||||
99
userfront/lib/core/i18n/locale_registry.dart
Normal file
99
userfront/lib/core/i18n/locale_registry.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const _translationAssetPrefix = 'assets/translations/';
|
||||
const _templateFileName = 'template.toml';
|
||||
const _safeFallbackLocaleCode = 'en';
|
||||
|
||||
List<String> extractSupportedLocaleCodesFromAssets(Iterable<String> assets) {
|
||||
final localeCodes = <String>{};
|
||||
for (final asset in assets) {
|
||||
if (!asset.startsWith(_translationAssetPrefix) ||
|
||||
!asset.endsWith('.toml')) {
|
||||
continue;
|
||||
}
|
||||
final fileName = asset.substring(_translationAssetPrefix.length);
|
||||
if (fileName.contains('/') || fileName == _templateFileName) {
|
||||
continue;
|
||||
}
|
||||
final rawCode = fileName.substring(0, fileName.length - '.toml'.length);
|
||||
final normalized = rawCode.toLowerCase().replaceAll('_', '-');
|
||||
if (_isValidLocaleCode(normalized)) {
|
||||
localeCodes.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
final sorted = localeCodes.toList()..sort();
|
||||
return sorted;
|
||||
}
|
||||
|
||||
class LocaleRegistry {
|
||||
static final Set<String> _localeCodes = <String>{};
|
||||
static bool _initialized = false;
|
||||
|
||||
static Future<void> initialize({AssetBundle? assetBundle}) async {
|
||||
if (_initialized) {
|
||||
return;
|
||||
}
|
||||
final bundle = assetBundle ?? rootBundle;
|
||||
try {
|
||||
final manifest = await AssetManifest.loadFromAssetBundle(bundle);
|
||||
final extracted = extractSupportedLocaleCodesFromAssets(
|
||||
manifest.listAssets(),
|
||||
);
|
||||
_localeCodes.addAll(extracted);
|
||||
} catch (_) {
|
||||
// manifest 로딩 실패 시 안전 fallback으로 계속 진행합니다.
|
||||
}
|
||||
|
||||
if (_localeCodes.isEmpty) {
|
||||
_localeCodes.add(_safeFallbackLocaleCode);
|
||||
}
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
static List<String> get supportedLocaleCodes {
|
||||
final sorted = _localeCodes.toList()..sort();
|
||||
return List.unmodifiable(sorted);
|
||||
}
|
||||
|
||||
static String get fallbackLocaleCode {
|
||||
final supported = supportedLocaleCodes;
|
||||
if (supported.isEmpty) {
|
||||
return _safeFallbackLocaleCode;
|
||||
}
|
||||
if (supported.contains('en')) {
|
||||
return 'en';
|
||||
}
|
||||
return supported.first;
|
||||
}
|
||||
|
||||
static bool contains(String code) {
|
||||
return _localeCodes.contains(code.toLowerCase());
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void setSupportedLocaleCodesForTest(Iterable<String> localeCodes) {
|
||||
_localeCodes
|
||||
..clear()
|
||||
..addAll(
|
||||
localeCodes
|
||||
.map((code) => code.toLowerCase().replaceAll('_', '-'))
|
||||
.where(_isValidLocaleCode),
|
||||
);
|
||||
if (_localeCodes.isEmpty) {
|
||||
_localeCodes.add(_safeFallbackLocaleCode);
|
||||
}
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void resetForTest() {
|
||||
_localeCodes.clear();
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isValidLocaleCode(String value) {
|
||||
return RegExp(r'^[a-z]{2,3}$').hasMatch(value);
|
||||
}
|
||||
@@ -1,34 +1,46 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'locale_storage.dart';
|
||||
import 'locale_registry.dart';
|
||||
|
||||
const supportedLocaleCodes = ['en', 'ko'];
|
||||
const defaultLocaleCode = 'en';
|
||||
String get defaultLocaleCode => LocaleRegistry.fallbackLocaleCode;
|
||||
|
||||
String normalizeLocaleCode(String? code) {
|
||||
final supportedLocaleCodes = LocaleRegistry.supportedLocaleCodes;
|
||||
final fallbackLocaleCode = LocaleRegistry.fallbackLocaleCode;
|
||||
|
||||
if (code == null || code.isEmpty) {
|
||||
return defaultLocaleCode;
|
||||
return fallbackLocaleCode;
|
||||
}
|
||||
final normalized = code.toLowerCase();
|
||||
if (normalized == 'ko' || normalized.startsWith('ko-')) {
|
||||
return 'ko';
|
||||
final normalized = code.toLowerCase().replaceAll('_', '-');
|
||||
if (supportedLocaleCodes.contains(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
if (normalized == 'en' || normalized.startsWith('en-')) {
|
||||
return 'en';
|
||||
final languageCode = normalized.split('-').first;
|
||||
if (supportedLocaleCodes.contains(languageCode)) {
|
||||
return languageCode;
|
||||
}
|
||||
return defaultLocaleCode;
|
||||
return fallbackLocaleCode;
|
||||
}
|
||||
|
||||
String resolvePreferredLocaleCode() {
|
||||
final stored = LocaleStorage.read();
|
||||
if (stored != null && supportedLocaleCodes.contains(stored)) {
|
||||
return stored;
|
||||
if (stored != null && stored.isNotEmpty) {
|
||||
final normalizedStored = normalizeLocaleCode(stored);
|
||||
if (LocaleRegistry.contains(normalizedStored)) {
|
||||
return normalizedStored;
|
||||
}
|
||||
}
|
||||
final deviceLocale = PlatformDispatcher.instance.locale;
|
||||
return normalizeLocaleCode(deviceLocale.languageCode);
|
||||
final languageTag =
|
||||
deviceLocale.countryCode == null || deviceLocale.countryCode!.isEmpty
|
||||
? deviceLocale.languageCode
|
||||
: '${deviceLocale.languageCode}-${deviceLocale.countryCode}';
|
||||
return normalizeLocaleCode(languageTag);
|
||||
}
|
||||
|
||||
String? extractLocaleFromPath(Uri uri) {
|
||||
final supportedLocaleCodes = LocaleRegistry.supportedLocaleCodes;
|
||||
if (uri.pathSegments.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
@@ -40,8 +52,10 @@ String? extractLocaleFromPath(Uri uri) {
|
||||
}
|
||||
|
||||
String stripLocalePath(Uri uri) {
|
||||
final supportedLocaleCodes = LocaleRegistry.supportedLocaleCodes;
|
||||
final segments = uri.pathSegments;
|
||||
if (segments.isNotEmpty && supportedLocaleCodes.contains(segments.first)) {
|
||||
if (segments.isNotEmpty &&
|
||||
supportedLocaleCodes.contains(segments.first.toLowerCase())) {
|
||||
final rest = segments.skip(1).join('/');
|
||||
if (rest.isEmpty) {
|
||||
return '/';
|
||||
@@ -52,18 +66,23 @@ String stripLocalePath(Uri uri) {
|
||||
}
|
||||
|
||||
String buildLocalizedPath(String localeCode, Uri uri) {
|
||||
final supportedLocaleCodes = LocaleRegistry.supportedLocaleCodes;
|
||||
final segments = uri.pathSegments;
|
||||
Iterable<String> restSegments = segments;
|
||||
if (segments.isNotEmpty) {
|
||||
final head = segments.first.toLowerCase();
|
||||
if (supportedLocaleCodes.contains(head) || head.length == 2) {
|
||||
if (supportedLocaleCodes.contains(head)) {
|
||||
restSegments = segments.skip(1);
|
||||
}
|
||||
}
|
||||
final newSegments = [localeCode, ...restSegments];
|
||||
final path = '/${newSegments.join('/')}';
|
||||
if (uri.queryParameters.isEmpty) {
|
||||
return path;
|
||||
}
|
||||
return Uri(path: path, queryParameters: uri.queryParameters).toString();
|
||||
final queryPart = uri.hasQuery ? '?${uri.query}' : '';
|
||||
final fragmentPart = uri.fragment.isNotEmpty ? '#${uri.fragment}' : '';
|
||||
return '$path$queryPart$fragmentPart';
|
||||
}
|
||||
|
||||
String buildSigninRedirectPath(String localeCode, Uri uri) {
|
||||
final queryPart = uri.hasQuery ? '?${uri.query}' : '';
|
||||
return '/$localeCode/signin$queryPart';
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import 'core/services/auth_token_store.dart';
|
||||
import 'core/services/logger_service.dart';
|
||||
import 'core/notifiers/auth_notifier.dart';
|
||||
import 'core/i18n/locale_gate.dart';
|
||||
import 'core/i18n/locale_registry.dart';
|
||||
import 'core/i18n/locale_utils.dart';
|
||||
import 'core/i18n/toml_asset_loader.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
@@ -45,6 +46,7 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
usePathUrlStrategy();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
await LocaleRegistry.initialize();
|
||||
|
||||
// 1. Global Error Handling
|
||||
FlutterError.onError = (details) {
|
||||
@@ -78,11 +80,16 @@ void main() async {
|
||||
runApp(
|
||||
// URL(/en, /ko)이 있으면 우선 적용해서 첫 렌더부터 올바른 언어로 시작합니다.
|
||||
() {
|
||||
final supportedLocaleCodes = LocaleRegistry.supportedLocaleCodes;
|
||||
final supportedLocales = supportedLocaleCodes
|
||||
.map((code) => Locale(code))
|
||||
.toList(growable: false);
|
||||
final fallbackLocaleCode = LocaleRegistry.fallbackLocaleCode;
|
||||
final initialLocaleCode =
|
||||
extractLocaleFromPath(Uri.base) ?? resolvePreferredLocaleCode();
|
||||
return EasyLocalization(
|
||||
supportedLocales: const [Locale('en'), Locale('ko')],
|
||||
fallbackLocale: const Locale('en'),
|
||||
supportedLocales: supportedLocales,
|
||||
fallbackLocale: Locale(fallbackLocaleCode),
|
||||
startLocale: Locale(initialLocaleCode),
|
||||
saveLocale: false,
|
||||
path: 'assets/translations',
|
||||
@@ -143,8 +150,13 @@ final _router = GoRouter(
|
||||
final redirectUrl =
|
||||
state.uri.queryParameters['redirect_uri'] ??
|
||||
state.uri.queryParameters['redirect_url'];
|
||||
_routerLogger.info("Navigating to /login, redirect: $redirectUrl");
|
||||
return LoginScreen(key: state.pageKey, redirectUrl: redirectUrl);
|
||||
_routerLogger.info(
|
||||
"Navigating to /login, redirect: $redirectUrl",
|
||||
);
|
||||
return LoginScreen(
|
||||
key: state.pageKey,
|
||||
redirectUrl: redirectUrl,
|
||||
);
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@@ -153,7 +165,9 @@ final _router = GoRouter(
|
||||
final consentChallenge =
|
||||
state.uri.queryParameters['consent_challenge'];
|
||||
if (consentChallenge == null) {
|
||||
_routerLogger.warning("Consent screen loaded without a challenge.");
|
||||
_routerLogger.warning(
|
||||
"Consent screen loaded without a challenge.",
|
||||
);
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('Error: Consent challenge is missing.'),
|
||||
@@ -241,8 +255,7 @@ final _router = GoRouter(
|
||||
return ErrorScreen(
|
||||
errorId: params['id'],
|
||||
errorCode: params['error'],
|
||||
description:
|
||||
params['error_description'] ?? params['message'],
|
||||
description: params['error_description'] ?? params['message'],
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -252,9 +265,7 @@ final _router = GoRouter(
|
||||
_routerLogger.info("Navigating to /settings (disabled)");
|
||||
return ErrorScreen(
|
||||
errorCode: 'settings_disabled',
|
||||
description: tr(
|
||||
'msg.userfront.settings.disabled',
|
||||
),
|
||||
description: tr('msg.userfront.settings.disabled'),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -333,13 +344,7 @@ final _router = GoRouter(
|
||||
// If not logged in and trying to access a protected page, redirect to /signin
|
||||
if (!isLoggedIn) {
|
||||
_routerLogger.info("Not logged in, redirecting to /signin");
|
||||
// Preserve OIDC challenge if present
|
||||
final loginChallenge = state.uri.queryParameters['login_challenge'];
|
||||
final locale = requestedLocale;
|
||||
if (loginChallenge != null) {
|
||||
return '/$locale/signin?login_challenge=$loginChallenge';
|
||||
}
|
||||
return '/$locale/signin';
|
||||
return buildSigninRedirectPath(requestedLocale, state.uri);
|
||||
}
|
||||
|
||||
// If logged in and trying to access login page, redirect to root (dashboard)
|
||||
|
||||
36
userfront/test/locale_registry_test.dart
Normal file
36
userfront/test/locale_registry_test.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/core/i18n/locale_registry.dart';
|
||||
|
||||
void main() {
|
||||
tearDown(() {
|
||||
LocaleRegistry.resetForTest();
|
||||
});
|
||||
|
||||
group('locale_registry', () {
|
||||
test(
|
||||
'extractSupportedLocaleCodesFromAssets excludes template and invalid',
|
||||
() {
|
||||
final locales = extractSupportedLocaleCodesFromAssets([
|
||||
'assets/translations/template.toml',
|
||||
'assets/translations/en.toml',
|
||||
'assets/translations/ko.toml',
|
||||
'assets/translations/pt_BR.toml',
|
||||
'assets/translations/readme.txt',
|
||||
'assets/translations/nested/ja.toml',
|
||||
]);
|
||||
|
||||
expect(locales, ['en', 'ko']);
|
||||
},
|
||||
);
|
||||
|
||||
test('fallback locale prefers en when available', () {
|
||||
LocaleRegistry.setSupportedLocaleCodesForTest(['ko', 'en']);
|
||||
expect(LocaleRegistry.fallbackLocaleCode, 'en');
|
||||
});
|
||||
|
||||
test('fallback locale uses first sorted code when en is absent', () {
|
||||
LocaleRegistry.setSupportedLocaleCodesForTest(['ko', 'ja']);
|
||||
expect(LocaleRegistry.fallbackLocaleCode, 'ja');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/core/i18n/locale_registry.dart';
|
||||
import 'package:userfront/core/i18n/locale_utils.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
LocaleRegistry.setSupportedLocaleCodesForTest(['en', 'ko']);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
LocaleRegistry.resetForTest();
|
||||
});
|
||||
|
||||
group('locale_utils', () {
|
||||
test('normalizeLocaleCode handles supported locales', () {
|
||||
expect(normalizeLocaleCode('ko'), 'ko');
|
||||
@@ -50,7 +59,25 @@ void main() {
|
||||
final uri = Uri.parse('/signin?redirect_uri=https://example.com');
|
||||
expect(
|
||||
buildLocalizedPath('ko', uri),
|
||||
'/ko/signin?redirect_uri=https%3A%2F%2Fexample.com',
|
||||
'/ko/signin?redirect_uri=https://example.com',
|
||||
);
|
||||
});
|
||||
|
||||
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',
|
||||
);
|
||||
expect(
|
||||
buildLocalizedPath('ko', uri),
|
||||
'/ko/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildLocalizedPath preserves fragment', () {
|
||||
final uri = Uri.parse('/signin?notice=qr_login_required#auth');
|
||||
expect(
|
||||
buildLocalizedPath('ko', uri),
|
||||
'/ko/signin?notice=qr_login_required#auth',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -59,8 +86,28 @@ void main() {
|
||||
expect(buildLocalizedPath('ko', Uri.parse('/en/profile')), '/ko/profile');
|
||||
});
|
||||
|
||||
test('buildLocalizedPath drops unknown 2-letter prefix', () {
|
||||
expect(buildLocalizedPath('ko', Uri.parse('/zz/signin')), '/ko/signin');
|
||||
test('buildLocalizedPath keeps unknown 2-letter prefix as path', () {
|
||||
expect(
|
||||
buildLocalizedPath('ko', Uri.parse('/zz/signin')),
|
||||
'/ko/zz/signin',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildSigninRedirectPath keeps path without query', () {
|
||||
expect(
|
||||
buildSigninRedirectPath('ko', Uri.parse('/ko/profile')),
|
||||
'/ko/signin',
|
||||
);
|
||||
});
|
||||
|
||||
test('buildSigninRedirectPath preserves full raw query', () {
|
||||
final uri = Uri.parse(
|
||||
'/ko/profile?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2¬ice=qr_login_required',
|
||||
);
|
||||
expect(
|
||||
buildSigninRedirectPath('ko', uri),
|
||||
'/ko/signin?a=1&a=2&redirect_uri=https%3A%2F%2Fexample.com%2Fcb%3Fx%3D1%26y%3D2¬ice=qr_login_required',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user