로그인 4종(Google·Apple·Kakao·LINE) 전환 · 이메일 가입 비노출 · 계정 연결 · STATION 탭 비노출 플래그

This commit is contained in:
Sn-Kinos
2026-09-11 02:56:41 +09:00
parent 4f2974b88c
commit d9b6b7c7a0
10 changed files with 910 additions and 259 deletions
+106 -7
View File
@@ -1,6 +1,8 @@
import 'dart:async';
import 'dart:io';
import 'package:fancake/app.dart';
import 'package:fancake/core/config/feature_flags.dart';
import 'package:fancake/core/firebase/firebase_boot.dart';
import 'package:fancake/core/router/app_router.dart';
import 'package:fancake/features/auth/auth.dart';
@@ -23,12 +25,60 @@ void main() {
expect(tester.takeException(), isNull);
expect(find.text('FANUZU'), findsOneWidget);
// 시안 S-A-LOGIN 의 소셜 버튼 4종이 라벨 그대로 있다.
for (final provider in SocialProvider.values) {
expect(find.text(provider.label), findsOneWidget, reason: provider.name);
// 별첨 B v1.4 확정 4종이 라벨 그대로 있다.
for (final provider in AuthRepository().providers) {
expect(
find.text(provider.provider.label),
findsOneWidget,
reason: provider.provider.name,
);
}
});
// 별첨 B v1.4 — *이메일/비밀번호 가입 및 X 로그인은 제외합니다.*
// 발주사가 「질의 없이 임의 변경」을 지적한 뒤의 회신이라 회귀 가드를 건다.
group('이메일 가입은 사용자에게 노출되지 않는다', () {
test('노출 제공자 목록은 Google·Apple·Kakao·LINE 4종뿐이다', () {
final kinds = AuthRepository().providers.map((p) => p.provider).toList();
expect(kinds, const [
AuthProviderKind.google,
AuthProviderKind.apple,
AuthProviderKind.kakao,
AuthProviderKind.line,
]);
expect(kinds, isNot(contains(AuthProviderKind.email)));
});
testWidgets('로그인 화면에 이메일 버튼·가입·재설정 입구가 없다', (tester) async {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 2;
addTearDown(tester.view.reset);
await tester.pumpWidget(FancakeApp(auth: AuthRepository()));
await tester.pumpAndSettle();
expect(find.text(AuthProviderKind.email.label), findsNothing);
expect(find.text('이메일'), findsNothing);
expect(find.text('비밀번호'), findsNothing);
expect(find.text('계정 만들기'), findsNothing);
expect(find.text('비밀번호 재설정'), findsNothing);
});
test('릴리스 빌드에는 QA 이메일 경로가 아예 없다', () async {
final auth = AuthRepository(releaseMode: true);
expect(auth.qaEmailProvider, isNull);
final outcome = await auth.signInWithEmail(
email: 'qa-test@fanuzu.co.kr',
password: 'password12',
);
expect(outcome, isA<AuthFailed>());
expect(auth.isSignedIn, isFalse);
});
test('디버그 빌드에서는 QA 이메일 경로가 살아 있다', () {
expect(AuthRepository(releaseMode: false).qaEmailProvider, isNotNull);
});
});
testWidgets('저장된 세션이 있으면 로그인 화면 대신 5탭 셸로 복원된다', (tester) async {
final session = _SmokeSession(
const AuthAccount(uid: 'restored-user', provider: SocialProvider.google),
@@ -56,9 +106,58 @@ void main() {
.map((d) => d.label)
.toList();
// 하드코딩이 아니라 계약 원본(design/craft-allowlist.yaml)과 대조한다.
expect(rendered, _contractTabs());
expect(rendered, appTabs.map((t) => t.label).toList());
// 별첨 B v1.4 — STATION 은 사용자 탭에서 비노출. 계약(5탭)은 그대로 두고
// 화면에 그리는 목록만 줄인다.
expect(rendered, ['HOME', 'INSIDE', 'GALAXY', 'MY']);
expect(rendered, visibleTabs(FeatureFlags()).map((t) => t.label).toList());
expect(find.text('STATION'), findsNothing);
});
// 계약(견적서 v2.1 · craft-allowlist.yaml · 시안 탭바)은 5탭 그대로다.
// 화면에서 뺀 것과 계약에서 뺀 것은 다른 일이고, 뒤엣것은 하지 않았다.
test('2-1. 계약 고정 5탭 표기·순서는 그대로다', () {
expect(appTabs.map((t) => t.label).toList(), _contractTabs());
});
group('STATION 은 원격 플래그로 켠다 (별첨 B v1.4 · 재배포 없이)', () {
test('플래그 기본값은 꺼짐이다', () {
expect(FeatureFlags().stationTab, isFalse);
expect(visibleTabs(FeatureFlags()), hasLength(4));
});
test('config/features 의 stationEnabled 가 true 면 5탭이 된다', () async {
final source = StreamController<Map<String, Object?>>();
final flags = FeatureFlags(source: source.stream);
addTearDown(flags.dispose);
source.add({FeatureFlags.stationKey: true});
await Future<void>.delayed(Duration.zero);
expect(flags.stationTab, isTrue);
expect(
visibleTabs(flags).map((t) => t.label).toList(),
appTabs.map((t) => t.label).toList(),
);
await source.close();
});
test('문자열 "true" 나 읽기 실패로는 켜지지 않는다', () async {
final source = StreamController<Map<String, Object?>>();
final flags = FeatureFlags(source: source.stream);
addTearDown(flags.dispose);
source.add({FeatureFlags.stationKey: 'true'});
source.addError(StateError('permission-denied'));
await Future<void>.delayed(Duration.zero);
expect(flags.stationTab, isFalse);
await source.close();
});
test('STATION 라우트는 남아 있다 — 비노출이지 삭제가 아니다', () {
final station = appTabs.firstWhere((t) => t.label == 'STATION');
expect(station.path, '/station');
});
});
// A-01 회귀 가드 — 목 우회 입구가 배포 앱에 새어 나가지 않는지.
@@ -143,7 +242,7 @@ void main() {
'HOME': ('S-H-HOME', '오늘의 퀘스트'),
'INSIDE': ('S-I-LOUNGE', '팬덤 라운지'),
'GALAXY': ('S-G-PLANET', '내 행성'),
'STATION': ('S-S-HUB', 'STATION 허브'),
// STATION 은 하단 탭에 없다(별첨 B v1.4). 나머지 4탭을 확인한다.
'MY': ('S-M-HOME', '마이 페이지'),
};