Flutter 앱 골격 · 5탭 네비게이션 · 로그인 화면 · Firestore 보안 규칙·인덱스
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fancake/app.dart';
|
||||
import 'package:fancake/core/firebase/firebase_boot.dart';
|
||||
import 'package:fancake/core/router/app_router.dart';
|
||||
import 'package:fancake/features/auth/auth.dart';
|
||||
import 'package:fancake/features/auth/auth_repository.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// 2주차 스모크 — Firebase 설정 파일이 **없는 상태**(목 모드)에서 전부 통과해야 한다.
|
||||
void main() {
|
||||
setUp(() {
|
||||
FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: '스모크 테스트 — 설정 파일 미투입 상황 재현');
|
||||
});
|
||||
|
||||
testWidgets('1. 앱이 예외 없이 뜨고 로그인 화면이 나온다', (tester) async {
|
||||
await tester.pumpWidget(FancakeApp(auth: AuthRepository()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('Fancake'), findsOneWidget);
|
||||
// 시안 S-A-LOGIN 의 소셜 버튼 4종이 라벨 그대로 있다.
|
||||
for (final provider in SocialProvider.values) {
|
||||
expect(find.text(provider.label), findsOneWidget, reason: provider.name);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('2. 하단 탭 5개가 계약 표기·순서 그대로 렌더된다', (tester) async {
|
||||
await _enterShell(tester);
|
||||
|
||||
final rendered = tester
|
||||
.widgetList<NavigationDestination>(
|
||||
find.descendant(
|
||||
of: find.byType(NavigationBar),
|
||||
matching: find.byType(NavigationDestination),
|
||||
),
|
||||
)
|
||||
.map((d) => d.label)
|
||||
.toList();
|
||||
|
||||
// 하드코딩이 아니라 계약 원본(design/craft-allowlist.yaml)과 대조한다.
|
||||
expect(rendered, _contractTabs());
|
||||
expect(rendered, appTabs.map((t) => t.label).toList());
|
||||
});
|
||||
|
||||
// A-01 회귀 가드 — 목 우회 입구가 배포 앱에 새어 나가지 않는지.
|
||||
// FirebaseBoot.init() 은 모든 예외를 삼키고 목 모드로 내려가므로, 설정 파일이 정상
|
||||
// 투입된 릴리스 빌드라도 초기화가 실패하면 목 모드가 된다. 그 상태를 그대로 재현한다.
|
||||
group('릴리스 빌드에서는 목 우회 입구가 열리지 않는다', () {
|
||||
test('목 모드 + 릴리스면 isMockSessionAvailable 이 false 다', () {
|
||||
final auth = AuthRepository(releaseMode: true);
|
||||
expect(FirebaseBoot.isLive, isFalse, reason: '목 모드 전제 확인');
|
||||
expect(auth.isMockSessionAvailable, isFalse);
|
||||
});
|
||||
|
||||
test('릴리스에서 signInWithMockSession 을 불러도 로그인되지 않는다', () {
|
||||
final auth = AuthRepository(releaseMode: true)..signInWithMockSession();
|
||||
expect(auth.isSignedIn, isFalse);
|
||||
expect(auth.account, isNull);
|
||||
});
|
||||
|
||||
testWidgets('릴리스 빌드 로그인 화면에 목 세션 버튼이 없다', (tester) async {
|
||||
await tester.pumpWidget(FancakeApp(auth: AuthRepository(releaseMode: true)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('목 세션으로 탭 둘러보기'), findsNothing);
|
||||
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.pumpAndSettle();
|
||||
|
||||
expect(find.text('목 세션으로 탭 둘러보기'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
// 화면에 나가는 문자열 전체를 훑는다. reason 만 고쳤다가 제공자 missingConfig 로
|
||||
// 리포 경로가 그대로 새어 나간 적이 있다(에뮬레이터 실기동 중 발견).
|
||||
test('제공자 4종의 미투입 안내에 내부 파일 경로가 들어가지 않는다', () {
|
||||
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) {
|
||||
expect(
|
||||
item.contains(bad),
|
||||
isFalse,
|
||||
reason: '${p.provider.name} 의 미투입 안내에 "$bad" 가 들어 있다: $item',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('화면에 띄우는 사유에 내부 파일 경로가 들어가지 않는다', () {
|
||||
FirebaseBoot.debugSetMode(
|
||||
FirebaseMode.mock,
|
||||
reason: 'Firebase 설정이 아직 투입되지 않아 임시(목) 모드로 동작 중입니다.',
|
||||
);
|
||||
expect(FirebaseBoot.reason, isNot(contains('google-services.json')));
|
||||
expect(FirebaseBoot.reason, isNot(contains('GoogleService-Info.plist')));
|
||||
expect(FirebaseBoot.reason, isNot(contains('/')));
|
||||
});
|
||||
|
||||
testWidgets('3. 탭을 전환하면 각 탭 루트 화면이 바뀐다', (tester) async {
|
||||
await _enterShell(tester);
|
||||
|
||||
const roots = {
|
||||
'HOME': ('S-H-HOME', '오늘의 퀘스트'),
|
||||
'INSIDE': ('S-I-LOUNGE', '팬덤 라운지'),
|
||||
'GALAXY': ('S-G-PLANET', '내 행성'),
|
||||
'STATION': ('S-S-HUB', 'STATION 허브'),
|
||||
'MY': ('S-M-HOME', '마이 페이지'),
|
||||
};
|
||||
|
||||
for (final entry in roots.entries) {
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(NavigationBar),
|
||||
matching: find.text(entry.key),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final (screenId, screenName) = entry.value;
|
||||
expect(find.text(screenId), findsOneWidget, reason: '${entry.key} → $screenId');
|
||||
// AppBar 제목과 본문 제목 두 군데에 나온다.
|
||||
expect(find.text(screenName), findsWidgets, reason: '${entry.key} → $screenName');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 로그인 화면에서 목 세션으로 들어가 5탭 셸까지 도달한다.
|
||||
///
|
||||
/// 실 소셜 로그인은 Firebase 설정 파일이 없으면 물리적으로 불가능하므로,
|
||||
/// 목 모드 전용 입구를 쓴다(AuthRepository.isMockSessionAvailable).
|
||||
Future<void> _enterShell(WidgetTester tester) async {
|
||||
await tester.pumpWidget(FancakeApp(auth: AuthRepository()));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final enter = find.text('목 세션으로 탭 둘러보기');
|
||||
await tester.ensureVisible(enter);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(enter);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(NavigationBar), findsOneWidget);
|
||||
}
|
||||
|
||||
/// `design/craft-allowlist.yaml` 의 `APP_TABS` 를 읽어 온다.
|
||||
/// 앱 코드가 계약 표기에서 벗어나면 여기서 깨진다.
|
||||
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}'),
|
||||
);
|
||||
final inner = RegExp(r'\[(.*)\]').firstMatch(line)!.group(1)!;
|
||||
return inner.split(',').map((s) => s.trim()).toList();
|
||||
}
|
||||
Reference in New Issue
Block a user