로그인 4종(Google·Apple·Kakao·LINE) 전환 · 이메일 가입 비노출 · 계정 연결 · STATION 탭 비노출 플래그
This commit is contained in:
@@ -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', '마이 페이지'),
|
||||
};
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ import 'package:fancake/features/auth/login_screen.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// 이메일 로그인 — 정책 설계서 §P-01 확정 가입 수단.
|
||||
/// 이메일 로그인 — **QA 계정 전용 내부 경로**.
|
||||
///
|
||||
/// 초안 당시 CD-18 미확정이라 비워 두었던 자리이며, 답변서 v1.1 Q7 이
|
||||
/// **이메일 · Google · Apple · Kakao · LINE 5종**으로 확정했다.
|
||||
/// 별첨 B v1.4 가 1차 로그인을 Google/Apple/Kakao/LINE 4종으로 확정하고
|
||||
/// 이메일/비밀번호 가입을 제외했다. 그래서 여기서 지키는 것은 두 가지다:
|
||||
/// ① 사용자에게 노출되는 목록에 이메일이 없을 것
|
||||
/// ② 그럼에도 발주사 QA 계정(qa-test@fanuzu.co.kr)으로는 들어갈 수 있을 것 —
|
||||
/// 단 디버그 빌드에서 숨은 입구(로고 길게 누르기)로만.
|
||||
|
||||
/// 이메일 제공자 대역. Firebase 없이 결과만 돌려준다.
|
||||
class _FakeEmail implements EmailAuthProvider {
|
||||
@@ -71,13 +74,21 @@ Future<void> tapVisible(WidgetTester tester, Finder finder) async {
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('확정 가입 수단 5종', () {
|
||||
test('네이버가 빠지고 이메일·LINE 이 들어왔다', () {
|
||||
group('별첨 B v1.4 확정 4종', () {
|
||||
test('노출 목록은 Google·Apple·Kakao·LINE 이고 이메일·네이버·X 는 없다', () {
|
||||
final kinds = AuthRepository().providers.map((p) => p.provider).toList();
|
||||
expect(kinds, hasLength(5));
|
||||
expect(kinds, contains(AuthProviderKind.email));
|
||||
expect(kinds, hasLength(4));
|
||||
expect(kinds, isNot(contains(AuthProviderKind.email)));
|
||||
expect(kinds, contains(AuthProviderKind.line));
|
||||
expect(kinds.map((k) => k.name), isNot(contains('naver')));
|
||||
expect(kinds.map((k) => k.name), isNot(contains('x')));
|
||||
});
|
||||
|
||||
test('노출 목록에 없는 수단으로는 signIn 을 부를 수 없다', () {
|
||||
expect(
|
||||
() => AuthRepository().signIn(AuthProviderKind.email),
|
||||
throwsArgumentError,
|
||||
);
|
||||
});
|
||||
|
||||
test('이메일만 입력 폼을 요구한다', () {
|
||||
@@ -86,15 +97,16 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('저장소가 이메일 제공자를 찾아낸다', () {
|
||||
expect(AuthRepository().emailProvider, isNotNull);
|
||||
test('QA 이메일 경로는 디버그 빌드에만 있다', () {
|
||||
expect(AuthRepository(releaseMode: false).qaEmailProvider, isNotNull);
|
||||
expect(AuthRepository(releaseMode: true).qaEmailProvider, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('저장소 — 이메일', () {
|
||||
group('저장소 — QA 이메일', () {
|
||||
test('로그인에 성공하면 세션이 바뀐다', () async {
|
||||
final fake = _FakeEmail(result: const AuthSucceeded(_account));
|
||||
final auth = AuthRepository(providers: [fake]);
|
||||
final auth = AuthRepository(qaEmailProvider: fake, releaseMode: false);
|
||||
final outcome = await auth.signInWithEmail(
|
||||
email: 'fan@fanuzu.co.kr',
|
||||
password: 'password12',
|
||||
@@ -107,7 +119,7 @@ void main() {
|
||||
test('가입은 로그인과 다른 경로로 간다', () async {
|
||||
// 한 버튼에 합치면 오타로 친 이메일이 새 계정을 만들어 버린다.
|
||||
final fake = _FakeEmail(result: const AuthSucceeded(_account));
|
||||
final auth = AuthRepository(providers: [fake]);
|
||||
final auth = AuthRepository(qaEmailProvider: fake, releaseMode: false);
|
||||
await auth.signInWithEmail(
|
||||
email: 'fan@fanuzu.co.kr',
|
||||
password: 'password12',
|
||||
@@ -118,45 +130,73 @@ void main() {
|
||||
|
||||
test('실패하면 세션이 그대로다', () async {
|
||||
final auth = AuthRepository(
|
||||
providers: [_FakeEmail(result: const AuthFailed('E-AUTH-01', '확인해 주세요.'))],
|
||||
qaEmailProvider: _FakeEmail(
|
||||
result: const AuthFailed('E-AUTH-01', '확인해 주세요.'),
|
||||
),
|
||||
releaseMode: false,
|
||||
);
|
||||
final outcome = await auth.signInWithEmail(email: 'a@b.c', password: 'x');
|
||||
expect(outcome, isA<AuthFailed>());
|
||||
expect(auth.isSignedIn, isFalse);
|
||||
});
|
||||
|
||||
test('이메일 제공자가 없으면 실패로 닫는다', () async {
|
||||
final auth = AuthRepository(providers: const []);
|
||||
final outcome = await auth.signInWithEmail(email: 'a@b.c', password: 'x');
|
||||
test('릴리스 빌드에서는 QA 경로가 실패로 닫힌다', () async {
|
||||
final auth = AuthRepository(
|
||||
qaEmailProvider: _FakeEmail(result: const AuthSucceeded(_account)),
|
||||
releaseMode: true,
|
||||
);
|
||||
final outcome = await auth.signInWithEmail(
|
||||
email: 'qa-test@fanuzu.co.kr',
|
||||
password: 'password12',
|
||||
);
|
||||
expect(outcome, isA<AuthFailed>());
|
||||
expect(auth.isSignedIn, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('로그인 화면 — 이메일 폼', () {
|
||||
group('로그인 화면 — QA 이메일 폼(숨은 입구)', () {
|
||||
Future<AuthRepository> pump(WidgetTester tester, _FakeEmail fake) async {
|
||||
tester.view.physicalSize = const Size(1080, 2400);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
addTearDown(tester.view.reset);
|
||||
final auth = AuthRepository(providers: [fake]);
|
||||
final auth = AuthRepository(qaEmailProvider: fake, releaseMode: false);
|
||||
await tester.pumpWidget(MaterialApp(home: LoginScreen(auth: auth)));
|
||||
return auth;
|
||||
}
|
||||
|
||||
testWidgets('처음에는 폼이 접혀 있고 버튼을 누르면 펼쳐진다', (tester) async {
|
||||
/// 숨은 입구 — 로고를 길게 누른다. 보이는 진입점은 두지 않는다.
|
||||
Future<void> openQaForm(WidgetTester tester) async {
|
||||
await tester.longPress(find.text('FANUZU'));
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('보이는 입구가 없고 로고를 길게 눌러야 열린다', (tester) async {
|
||||
await pump(tester, _FakeEmail());
|
||||
expect(find.text('이메일'), findsNothing);
|
||||
expect(find.text(AuthProviderKind.email.label), findsNothing);
|
||||
|
||||
await tapVisible(tester, find.text(AuthProviderKind.email.label));
|
||||
await tester.pump();
|
||||
await openQaForm(tester);
|
||||
expect(find.text('이메일'), findsOneWidget);
|
||||
expect(find.text('비밀번호'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('릴리스 빌드에서는 로고를 길게 눌러도 열리지 않는다', (tester) async {
|
||||
tester.view.physicalSize = const Size(1080, 2400);
|
||||
tester.view.devicePixelRatio = 3.0;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: LoginScreen(auth: AuthRepository(releaseMode: true))),
|
||||
);
|
||||
await tester.longPress(find.text('FANUZU'));
|
||||
await tester.pump();
|
||||
expect(find.text('이메일'), findsNothing);
|
||||
expect(find.text('비밀번호'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('빈 값으로 제출하면 안내가 뜨고 호출하지 않는다', (tester) async {
|
||||
final fake = _FakeEmail();
|
||||
await pump(tester, fake);
|
||||
await tapVisible(tester, find.text(AuthProviderKind.email.label));
|
||||
await tester.pump();
|
||||
await openQaForm(tester);
|
||||
|
||||
await tapVisible(tester, find.widgetWithText(FilledButton, '로그인'));
|
||||
await tester.pump();
|
||||
@@ -167,8 +207,7 @@ void main() {
|
||||
testWidgets('이메일 형식이 아니면 막는다', (tester) async {
|
||||
final fake = _FakeEmail();
|
||||
await pump(tester, fake);
|
||||
await tapVisible(tester, find.text(AuthProviderKind.email.label));
|
||||
await tester.pump();
|
||||
await openQaForm(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField).first, 'fanuzu.co.kr');
|
||||
await tester.enterText(find.byType(TextFormField).last, 'password12');
|
||||
@@ -181,12 +220,14 @@ void main() {
|
||||
testWidgets('가입 모드에서는 짧은 비밀번호를 막는다', (tester) async {
|
||||
final fake = _FakeEmail();
|
||||
await pump(tester, fake);
|
||||
await tapVisible(tester, find.text(AuthProviderKind.email.label));
|
||||
await tester.pump();
|
||||
await openQaForm(tester);
|
||||
await tapVisible(tester, find.text('계정 만들기'));
|
||||
await tester.pump();
|
||||
|
||||
await tester.enterText(find.byType(TextFormField).first, 'fan@fanuzu.co.kr');
|
||||
await tester.enterText(
|
||||
find.byType(TextFormField).first,
|
||||
'fan@fanuzu.co.kr',
|
||||
);
|
||||
await tester.enterText(find.byType(TextFormField).last, 'short');
|
||||
await tapVisible(tester, find.widgetWithText(FilledButton, '가입하기'));
|
||||
await tester.pump();
|
||||
@@ -200,10 +241,12 @@ void main() {
|
||||
testWidgets('올바른 값이면 제공자를 부른다', (tester) async {
|
||||
final fake = _FakeEmail(result: const AuthSucceeded(_account));
|
||||
await pump(tester, fake);
|
||||
await tapVisible(tester, find.text(AuthProviderKind.email.label));
|
||||
await tester.pump();
|
||||
await openQaForm(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField).first, 'fan@fanuzu.co.kr');
|
||||
await tester.enterText(
|
||||
find.byType(TextFormField).first,
|
||||
'fan@fanuzu.co.kr',
|
||||
);
|
||||
await tester.enterText(find.byType(TextFormField).last, 'password12');
|
||||
await tapVisible(tester, find.widgetWithText(FilledButton, '로그인'));
|
||||
await tester.pump();
|
||||
@@ -215,8 +258,7 @@ void main() {
|
||||
testWidgets('재설정은 이메일이 비어 있으면 보내지 않는다', (tester) async {
|
||||
final fake = _FakeEmail();
|
||||
await pump(tester, fake);
|
||||
await tapVisible(tester, find.text(AuthProviderKind.email.label));
|
||||
await tester.pump();
|
||||
await openQaForm(tester);
|
||||
|
||||
await tapVisible(tester, find.text('비밀번호 재설정'));
|
||||
await tester.pump();
|
||||
@@ -227,10 +269,12 @@ void main() {
|
||||
testWidgets('재설정 안내는 계정 존재 여부를 드러내지 않는다', (tester) async {
|
||||
final fake = _FakeEmail();
|
||||
await pump(tester, fake);
|
||||
await tapVisible(tester, find.text(AuthProviderKind.email.label));
|
||||
await tester.pump();
|
||||
await openQaForm(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField).first, 'nobody@fanuzu.co.kr');
|
||||
await tester.enterText(
|
||||
find.byType(TextFormField).first,
|
||||
'nobody@fanuzu.co.kr',
|
||||
);
|
||||
await tapVisible(tester, find.text('비밀번호 재설정'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
|
||||
Reference in New Issue
Block a user