1
0
forked from baron/baron-sso

CI test 업데이트

This commit is contained in:
Lectom C Han
2026-02-20 09:43:19 +09:00
parent 5d8697e361
commit 8ed3bd8c77
11 changed files with 714 additions and 401 deletions

View File

@@ -0,0 +1,244 @@
import 'locale_storage_backend.dart';
import 'locale_storage_policy.dart';
enum _StorageTarget { local, session, memory }
abstract interface class LocaleStorageTarget {
String? read(String key);
bool write(String key, String value);
bool remove(String key);
void clear();
}
class LocaleStorageNoopTarget implements LocaleStorageTarget {
const LocaleStorageNoopTarget();
@override
String? read(String key) => null;
@override
bool write(String key, String value) => false;
@override
bool remove(String key) => false;
@override
void clear() {}
}
class LocaleStorageCallbackTarget implements LocaleStorageTarget {
LocaleStorageCallbackTarget({
required this.readCallback,
required this.writeCallback,
required this.removeCallback,
required this.clearCallback,
});
final String? Function(String key) readCallback;
final void Function(String key, String value) writeCallback;
final void Function(String key) removeCallback;
final void Function() clearCallback;
@override
String? read(String key) => readCallback(key);
@override
bool write(String key, String value) {
writeCallback(key, value);
return true;
}
@override
bool remove(String key) {
removeCallback(key);
return true;
}
@override
void clear() => clearCallback();
}
class LocaleStorageEngine implements LocaleStorageBackend {
LocaleStorageEngine({
required LocaleStorageTarget localTarget,
required LocaleStorageTarget sessionTarget,
}) : _localTarget = localTarget,
_sessionTarget = sessionTarget;
final LocaleStorageTarget _localTarget;
final LocaleStorageTarget _sessionTarget;
final Map<String, String> _memory = {};
LocaleStorageTestMode _mode = LocaleStorageTestMode.normal;
List<_StorageTarget> _fallbackTargets() {
switch (_mode) {
case LocaleStorageTestMode.normal:
return [
_StorageTarget.local,
_StorageTarget.session,
_StorageTarget.memory,
];
case LocaleStorageTestMode.sessionOnly:
return [_StorageTarget.session, _StorageTarget.memory];
case LocaleStorageTestMode.memoryOnly:
return [_StorageTarget.memory];
}
}
String? _safeReadTarget(_StorageTarget target, String key) {
try {
switch (target) {
case _StorageTarget.local:
return _localTarget.read(key);
case _StorageTarget.session:
return _sessionTarget.read(key);
case _StorageTarget.memory:
return _memory[key];
}
} catch (_) {
return null;
}
}
bool _safeWriteTarget(_StorageTarget target, String key, String value) {
try {
switch (target) {
case _StorageTarget.local:
return _localTarget.write(key, value);
case _StorageTarget.session:
return _sessionTarget.write(key, value);
case _StorageTarget.memory:
_memory[key] = value;
return true;
}
} catch (_) {
return false;
}
}
bool _safeRemoveTarget(_StorageTarget target, String key) {
try {
switch (target) {
case _StorageTarget.local:
return _localTarget.remove(key);
case _StorageTarget.session:
return _sessionTarget.remove(key);
case _StorageTarget.memory:
_memory.remove(key);
return true;
}
} catch (_) {
return false;
}
}
void _safeClearTarget(_StorageTarget target) {
try {
switch (target) {
case _StorageTarget.local:
_localTarget.clear();
case _StorageTarget.session:
_sessionTarget.clear();
case _StorageTarget.memory:
_memory.clear();
}
} catch (_) {
// 테스트 정리 단계에서는 clear 예외를 무시합니다.
}
}
String? _readByKey(String key) {
for (final target in _fallbackTargets()) {
final value = _safeReadTarget(target, key);
if (value != null) {
return value;
}
}
return null;
}
void _writeByKey(String key, String value) {
for (final target in _fallbackTargets()) {
if (_safeWriteTarget(target, key, value)) {
return;
}
}
}
void _removeEverywhere(String key) {
_safeRemoveTarget(_StorageTarget.local, key);
_safeRemoveTarget(_StorageTarget.session, key);
_memory.remove(key);
}
@override
String? read() {
final current = _readByKey(LocaleStoragePolicy.currentKey);
if (LocaleStoragePolicy.hasValue(current)) {
return current;
}
final legacy = _readByKey(LocaleStoragePolicy.legacyKey);
if (LocaleStoragePolicy.shouldMigrateLegacy(
current: current,
legacy: legacy,
)) {
_writeByKey(LocaleStoragePolicy.currentKey, legacy!);
_removeEverywhere(LocaleStoragePolicy.legacyKey);
return legacy;
}
return null;
}
@override
void write(String locale) {
_writeByKey(LocaleStoragePolicy.currentKey, locale);
}
@override
void setTestMode(LocaleStorageTestMode mode) {
_mode = mode;
}
@override
void clearForTests() {
_safeClearTarget(_StorageTarget.local);
_safeClearTarget(_StorageTarget.session);
_memory.clear();
_mode = LocaleStorageTestMode.normal;
}
@override
void seedLegacyForTests(String locale) {
_writeByKey(LocaleStoragePolicy.legacyKey, locale);
}
@override
LocaleStorageDebugState debugStateForTests() {
return LocaleStorageDebugState(
mode: _mode,
localCurrent: _safeReadTarget(
_StorageTarget.local,
LocaleStoragePolicy.currentKey,
),
localLegacy: _safeReadTarget(
_StorageTarget.local,
LocaleStoragePolicy.legacyKey,
),
sessionCurrent: _safeReadTarget(
_StorageTarget.session,
LocaleStoragePolicy.currentKey,
),
sessionLegacy: _safeReadTarget(
_StorageTarget.session,
LocaleStoragePolicy.legacyKey,
),
memoryCurrent: _memory[LocaleStoragePolicy.currentKey],
memoryLegacy: _memory[LocaleStoragePolicy.legacyKey],
);
}
}

View File

@@ -1,59 +1,7 @@
import 'locale_storage_backend.dart';
import 'locale_storage_policy.dart';
import 'locale_storage_engine.dart';
class LocaleStorageImpl implements LocaleStorageBackend {
final Map<String, String> _memory = {};
LocaleStorageTestMode _mode = LocaleStorageTestMode.normal;
@override
String? read() {
final current = _memory[LocaleStoragePolicy.currentKey];
if (LocaleStoragePolicy.hasValue(current)) {
return current;
}
final legacy = _memory[LocaleStoragePolicy.legacyKey];
if (LocaleStoragePolicy.shouldMigrateLegacy(
current: current,
legacy: legacy,
)) {
_memory[LocaleStoragePolicy.currentKey] = legacy!;
_memory.remove(LocaleStoragePolicy.legacyKey);
return legacy;
}
return null;
}
@override
void write(String locale) {
_memory[LocaleStoragePolicy.currentKey] = locale;
}
@override
void setTestMode(LocaleStorageTestMode mode) {
_mode = mode;
}
@override
void clearForTests() {
_memory.clear();
_mode = LocaleStorageTestMode.normal;
}
@override
void seedLegacyForTests(String locale) {
_memory[LocaleStoragePolicy.legacyKey] = locale;
}
@override
LocaleStorageDebugState debugStateForTests() {
return LocaleStorageDebugState(
mode: _mode,
memoryCurrent: _memory[LocaleStoragePolicy.currentKey],
memoryLegacy: _memory[LocaleStoragePolicy.legacyKey],
);
}
}
final LocaleStorageBackend localeStorage = LocaleStorageImpl();
final LocaleStorageBackend localeStorage = LocaleStorageEngine(
localTarget: const LocaleStorageNoopTarget(),
sessionTarget: const LocaleStorageNoopTarget(),
);

View File

@@ -3,198 +3,20 @@
import 'package:web/web.dart' as web;
import 'locale_storage_backend.dart';
import 'locale_storage_policy.dart';
import 'locale_storage_engine.dart';
enum _StorageTarget { local, session, memory }
class LocaleStorageImpl implements LocaleStorageBackend {
static final Map<String, String> _memory = {};
static LocaleStorageTestMode _mode = LocaleStorageTestMode.normal;
List<_StorageTarget> _fallbackTargets() {
switch (_mode) {
case LocaleStorageTestMode.normal:
return [
_StorageTarget.local,
_StorageTarget.session,
_StorageTarget.memory,
];
case LocaleStorageTestMode.sessionOnly:
return [_StorageTarget.session, _StorageTarget.memory];
case LocaleStorageTestMode.memoryOnly:
return [_StorageTarget.memory];
}
}
String? _safeReadLocal(String key) {
try {
return web.window.localStorage.getItem(key);
} catch (_) {
return null;
}
}
String? _safeReadSession(String key) {
try {
return web.window.sessionStorage.getItem(key);
} catch (_) {
return null;
}
}
bool _safeWriteLocal(String key, String value) {
try {
web.window.localStorage.setItem(key, value);
return true;
} catch (_) {
return false;
}
}
bool _safeWriteSession(String key, String value) {
try {
web.window.sessionStorage.setItem(key, value);
return true;
} catch (_) {
return false;
}
}
bool _safeRemoveLocal(String key) {
try {
web.window.localStorage.removeItem(key);
return true;
} catch (_) {
return false;
}
}
bool _safeRemoveSession(String key) {
try {
web.window.sessionStorage.removeItem(key);
return true;
} catch (_) {
return false;
}
}
void _safeClearLocal() {
try {
web.window.localStorage.clear();
} catch (_) {
// 스토리지 접근이 차단된 경우는 테스트 정리에서 무시합니다.
}
}
void _safeClearSession() {
try {
web.window.sessionStorage.clear();
} catch (_) {
// 스토리지 접근이 차단된 경우는 테스트 정리에서 무시합니다.
}
}
String? _readFromTarget(_StorageTarget target, String key) {
switch (target) {
case _StorageTarget.local:
return _safeReadLocal(key);
case _StorageTarget.session:
return _safeReadSession(key);
case _StorageTarget.memory:
return _memory[key];
}
}
bool _writeToTarget(_StorageTarget target, String key, String value) {
switch (target) {
case _StorageTarget.local:
return _safeWriteLocal(key, value);
case _StorageTarget.session:
return _safeWriteSession(key, value);
case _StorageTarget.memory:
_memory[key] = value;
return true;
}
}
String? _readByKey(String key) {
for (final target in _fallbackTargets()) {
final value = _readFromTarget(target, key);
if (value != null) {
return value;
}
}
return null;
}
void _writeByKey(String key, String value) {
for (final target in _fallbackTargets()) {
if (_writeToTarget(target, key, value)) {
return;
}
}
}
void _removeEverywhere(String key) {
_safeRemoveLocal(key);
_safeRemoveSession(key);
_memory.remove(key);
}
@override
String? read() {
final current = _readByKey(LocaleStoragePolicy.currentKey);
if (LocaleStoragePolicy.hasValue(current)) {
return current;
}
final legacy = _readByKey(LocaleStoragePolicy.legacyKey);
if (LocaleStoragePolicy.shouldMigrateLegacy(
current: current,
legacy: legacy,
)) {
_writeByKey(LocaleStoragePolicy.currentKey, legacy!);
_removeEverywhere(LocaleStoragePolicy.legacyKey);
return legacy;
}
return null;
}
@override
void write(String locale) {
_writeByKey(LocaleStoragePolicy.currentKey, locale);
}
@override
void setTestMode(LocaleStorageTestMode mode) {
_mode = mode;
}
@override
void clearForTests() {
_safeClearLocal();
_safeClearSession();
_memory.clear();
_mode = LocaleStorageTestMode.normal;
}
@override
void seedLegacyForTests(String locale) {
_writeByKey(LocaleStoragePolicy.legacyKey, locale);
}
@override
LocaleStorageDebugState debugStateForTests() {
return LocaleStorageDebugState(
mode: _mode,
localCurrent: _safeReadLocal(LocaleStoragePolicy.currentKey),
localLegacy: _safeReadLocal(LocaleStoragePolicy.legacyKey),
sessionCurrent: _safeReadSession(LocaleStoragePolicy.currentKey),
sessionLegacy: _safeReadSession(LocaleStoragePolicy.legacyKey),
memoryCurrent: _memory[LocaleStoragePolicy.currentKey],
memoryLegacy: _memory[LocaleStoragePolicy.legacyKey],
);
}
}
final LocaleStorageBackend localeStorage = LocaleStorageImpl();
final LocaleStorageBackend localeStorage = LocaleStorageEngine(
localTarget: LocaleStorageCallbackTarget(
readCallback: (key) => web.window.localStorage.getItem(key),
writeCallback: (key, value) => web.window.localStorage.setItem(key, value),
removeCallback: (key) => web.window.localStorage.removeItem(key),
clearCallback: () => web.window.localStorage.clear(),
),
sessionTarget: LocaleStorageCallbackTarget(
readCallback: (key) => web.window.sessionStorage.getItem(key),
writeCallback: (key, value) =>
web.window.sessionStorage.setItem(key, value),
removeCallback: (key) => web.window.sessionStorage.removeItem(key),
clearCallback: () => web.window.sessionStorage.clear(),
),
);

View File

@@ -1,75 +1,138 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:userfront/core/i18n/locale_storage.dart';
import 'package:userfront/core/i18n/locale_storage_backend.dart';
import 'package:userfront/core/i18n/locale_storage_engine.dart';
import 'package:userfront/core/i18n/locale_storage_policy.dart';
class _FakeTarget implements LocaleStorageTarget {
_FakeTarget();
final Map<String, String> store = {};
bool throwOnRead = false;
bool throwOnWrite = false;
bool throwOnRemove = false;
bool throwOnClear = false;
@override
String? read(String key) {
if (throwOnRead) {
throw StateError('read blocked');
}
return store[key];
}
@override
bool write(String key, String value) {
if (throwOnWrite) {
throw StateError('write blocked');
}
store[key] = value;
return true;
}
@override
bool remove(String key) {
if (throwOnRemove) {
throw StateError('remove blocked');
}
store.remove(key);
return true;
}
@override
void clear() {
if (throwOnClear) {
throw StateError('clear blocked');
}
store.clear();
}
}
void main() {
late _FakeTarget localTarget;
late _FakeTarget sessionTarget;
late LocaleStorageEngine engine;
setUp(() {
LocaleStorage.setTestModeForTests(LocaleStorageTestMode.normal);
LocaleStorage.clearForTests();
localTarget = _FakeTarget();
sessionTarget = _FakeTarget();
engine = LocaleStorageEngine(
localTarget: localTarget,
sessionTarget: sessionTarget,
);
engine.clearForTests();
});
tearDown(() {
LocaleStorage.setTestModeForTests(LocaleStorageTestMode.normal);
LocaleStorage.clearForTests();
});
test('기본 모드에서는 local 우선으로 저장/조회한다', () {
engine.write('ko');
expect(engine.read(), 'ko');
test('localStorage write/read (웹)', () {
if (!kIsWeb) {
return;
}
LocaleStorage.write('ko');
expect(LocaleStorage.read(), 'ko');
final state = LocaleStorage.debugStateForTests();
final state = engine.debugStateForTests();
expect(state.localCurrent, 'ko');
expect(state.sessionCurrent, isNull);
expect(state.memoryCurrent, isNull);
}, skip: !kIsWeb);
});
test('legacy key에서 locale로 마이그레이션 (웹)', () {
if (!kIsWeb) {
return;
}
test('legacy key를 읽으면 current key로 마이그레이션한다', () {
localTarget.store[LocaleStoragePolicy.legacyKey] = 'en';
LocaleStorage.seedLegacyForTests('en');
expect(LocaleStorage.read(), 'en');
expect(engine.read(), 'en');
final state = LocaleStorage.debugStateForTests();
final state = engine.debugStateForTests();
expect(state.localCurrent, 'en');
expect(state.localLegacy, isNull);
}, skip: !kIsWeb);
});
test('localStorage 접근이 차단되면 메모리 fallback (웹)', () {
if (!kIsWeb) {
return;
}
test('localStorage 차단되면 sessionStorage로 fallback 한다', () {
localTarget
..throwOnRead = true
..throwOnWrite = true
..throwOnRemove = true;
LocaleStorage.forceMemoryStorageForTests(true);
engine.write('ko');
expect(engine.read(), 'ko');
LocaleStorage.write('en');
expect(LocaleStorage.read(), 'en');
final state = LocaleStorage.debugStateForTests();
expect(state.localCurrent, isNull);
expect(state.sessionCurrent, isNull);
expect(state.memoryCurrent, 'en');
}, skip: !kIsWeb);
test('localStorage 접근이 차단되면 sessionStorage로 fallback (웹)', () {
if (!kIsWeb) {
return;
}
LocaleStorage.forceSessionStorageForTests(true);
LocaleStorage.write('ko');
expect(LocaleStorage.read(), 'ko');
final state = LocaleStorage.debugStateForTests();
final state = engine.debugStateForTests();
expect(state.localCurrent, isNull);
expect(state.sessionCurrent, 'ko');
expect(state.memoryCurrent, isNull);
}, skip: !kIsWeb);
});
test('local/session 모두 차단되면 memory fallback 한다', () {
localTarget
..throwOnRead = true
..throwOnWrite = true
..throwOnRemove = true;
sessionTarget
..throwOnRead = true
..throwOnWrite = true
..throwOnRemove = true;
engine.write('en');
expect(engine.read(), 'en');
final state = engine.debugStateForTests();
expect(state.localCurrent, isNull);
expect(state.sessionCurrent, isNull);
expect(state.memoryCurrent, 'en');
});
test('sessionOnly 모드에서는 session + memory만 사용한다', () {
engine.setTestMode(LocaleStorageTestMode.sessionOnly);
engine.write('ko');
final state = engine.debugStateForTests();
expect(state.localCurrent, isNull);
expect(state.sessionCurrent, 'ko');
expect(state.memoryCurrent, isNull);
});
test('memoryOnly 모드에서는 memory만 사용한다', () {
engine.setTestMode(LocaleStorageTestMode.memoryOnly);
engine.write('en');
final state = engine.debugStateForTests();
expect(state.localCurrent, isNull);
expect(state.sessionCurrent, isNull);
expect(state.memoryCurrent, 'en');
});
}

View File

@@ -152,4 +152,25 @@ void main() {
expect(find.text('profile-page'), findsOneWidget);
expect(find.textContaining('signin|'), findsNothing);
});
testWidgets('로그인 후 같은 브라우저 새 창/팝업에서도 세션이 유지된다', (tester) async {
await tester.pumpWidget(_buildTestApp('/en/signin'));
await tester.pumpAndSettle();
expect(find.textContaining('signin|'), findsOneWidget);
AuthTokenStore.setToken('persisted-token', provider: 'ory');
await tester.pumpWidget(const SizedBox.shrink());
await tester.pumpAndSettle();
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);
});
}