소셜 로그인 인증 경로 · 세션 전이 · Apple 로그인 콜백 · 인증 테스트

This commit is contained in:
Sn-Kinos
2026-08-18 02:06:29 +09:00
parent 97ea5fad35
commit 357f5d759d
20 changed files with 729 additions and 127 deletions
+71 -8
View File
@@ -11,7 +11,10 @@ import 'package:flutter_test/flutter_test.dart';
/// 2주차 스모크 — Firebase 설정 파일이 **없는 상태**(목 모드)에서 전부 통과해야 한다.
void main() {
setUp(() {
FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: '스모크 테스트 — 설정 파일 미투입 상황 재현');
FirebaseBoot.debugSetMode(
FirebaseMode.mock,
reason: '스모크 테스트 — 설정 파일 미투입 상황 재현',
);
});
testWidgets('1. 앱이 예외 없이 뜨고 로그인 화면이 나온다', (tester) async {
@@ -26,6 +29,20 @@ void main() {
}
});
testWidgets('저장된 세션이 있으면 로그인 화면 대신 5탭 셸로 복원된다', (tester) async {
final session = _SmokeSession(
const AuthAccount(uid: 'restored-user', provider: SocialProvider.google),
);
final auth = AuthRepository(session: session, releaseMode: true);
await tester.pumpWidget(FancakeApp(auth: auth));
await tester.pumpAndSettle();
expect(find.byType(NavigationBar), findsOneWidget);
expect(find.text('목 세션으로 탭 둘러보기'), findsNothing);
auth.dispose();
});
testWidgets('2. 하단 탭 5개가 계약 표기·순서 그대로 렌더된다', (tester) async {
await _enterShell(tester);
@@ -61,17 +78,25 @@ void main() {
});
testWidgets('릴리스 빌드 로그인 화면에 목 세션 버튼이 없다', (tester) async {
await tester.pumpWidget(FancakeApp(auth: AuthRepository(releaseMode: true)));
await tester.pumpWidget(
FancakeApp(auth: AuthRepository(releaseMode: true)),
);
await tester.pumpAndSettle();
expect(find.text('목 세션으로 탭 둘러보기'), findsNothing);
expect(find.byType(NavigationBar), findsNothing, reason: '셸에 진입할 수 없어야 한다');
expect(
find.byType(NavigationBar),
findsNothing,
reason: '셸에 진입할 수 없어야 한다',
);
// 소셜 버튼은 그대로 있어야 한다 — 우회 입구만 막는 것이지 로그인을 막는 게 아니다.
expect(find.text(SocialProvider.google.label), findsOneWidget);
});
testWidgets('디버그 빌드에서는 목 세션 버튼이 있다 (가드가 과하게 잠그지 않았는지)', (tester) async {
await tester.pumpWidget(FancakeApp(auth: AuthRepository(releaseMode: false)));
await tester.pumpWidget(
FancakeApp(auth: AuthRepository(releaseMode: false)),
);
await tester.pumpAndSettle();
expect(find.text('목 세션으로 탭 둘러보기'), findsOneWidget);
@@ -81,7 +106,13 @@ void main() {
// 화면에 나가는 문자열 전체를 훑는다. reason 만 고쳤다가 제공자 missingConfig 로
// 리포 경로가 그대로 새어 나간 적이 있다(에뮬레이터 실기동 중 발견).
test('제공자 4종의 미투입 안내에 내부 파일 경로가 들어가지 않는다', () {
const forbidden = ['google-services.json', 'GoogleService-Info.plist', 'app/', '.plist', '.json'];
const forbidden = [
'google-services.json',
'GoogleService-Info.plist',
'app/',
'.plist',
'.json',
];
for (final p in AuthRepository().providers) {
for (final item in p.missingConfig) {
for (final bad in forbidden) {
@@ -126,9 +157,27 @@ void main() {
await tester.pumpAndSettle();
final (screenId, screenName) = entry.value;
expect(find.text(screenId), findsOneWidget, reason: '${entry.key} → $screenId');
expect(
find.text(screenId),
findsOneWidget,
reason: '${entry.key} → $screenId',
);
// AppBar 제목과 본문 제목 두 군데에 나온다.
expect(find.text(screenName), findsWidgets, reason: '${entry.key} → $screenName');
expect(
find.text(screenName),
findsWidgets,
reason: '${entry.key} → $screenName',
);
expect(
find.textContaining('wireframe/'),
findsNothing,
reason: '${entry.key} 화면에 내부 저장소 경로가 노출되면 안 된다',
);
expect(
find.textContaining('screen-spec'),
findsNothing,
reason: '${entry.key} 화면에 내부 명세 경로가 노출되면 안 된다',
);
}
});
}
@@ -156,8 +205,22 @@ List<String> _contractTabs() {
final file = File('../design/craft-allowlist.yaml');
final line = file.readAsLinesSync().firstWhere(
(l) => l.trimLeft().startsWith('APP_TABS:'),
orElse: () => throw StateError('craft-allowlist.yaml 에 APP_TABS 가 없다: ${file.path}'),
orElse: () =>
throw StateError('craft-allowlist.yaml 에 APP_TABS 가 없다: ${file.path}'),
);
final inner = RegExp(r'\[(.*)\]').firstMatch(line)!.group(1)!;
return inner.split(',').map((s) => s.trim()).toList();
}
final class _SmokeSession implements AuthSession {
const _SmokeSession(this.currentAccount);
@override
final AuthAccount? currentAccount;
@override
Stream<AuthAccount?> get accountChanges => const Stream.empty();
@override
Future<void> signOut() async {}
}