import 'package:fancake/features/auth/auth.dart'; import 'package:fancake/features/auth/auth_repository.dart'; import 'package:fancake/features/auth/login_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; /// 이메일 로그인 — **QA 계정 전용 내부 경로**. /// /// 별첨 B v1.4 가 1차 로그인을 Google/Apple/Kakao/LINE 4종으로 확정하고 /// 이메일/비밀번호 가입을 제외했다. 그래서 여기서 지키는 것은 두 가지다: /// ① 사용자에게 노출되는 목록에 이메일이 없을 것 /// ② 그럼에도 발주사 QA 계정(qa-test@fanuzu.co.kr)으로는 들어갈 수 있을 것 — /// 단 디버그 빌드에서 숨은 입구(로고 길게 누르기)로만. /// 이메일 제공자 대역. Firebase 없이 결과만 돌려준다. class _FakeEmail implements EmailAuthProvider { _FakeEmail({this.result = const AuthCancelled()}); AuthOutcome result; String? lastEmail; String? lastPassword; bool lastWasSignUp = false; String? resetSentTo; @override AuthProviderKind get provider => AuthProviderKind.email; @override bool get isLive => true; @override List get missingConfig => const []; @override Future signIn() async => const AuthCancelled(); @override Future signInWithEmail(String email, String password) async { lastEmail = email; lastPassword = password; lastWasSignUp = false; return result; } @override Future signUpWithEmail(String email, String password) async { lastEmail = email; lastPassword = password; lastWasSignUp = true; return result; } @override Future sendPasswordReset(String email) async { resetSentTo = email; return const AuthCancelled(); } @override Future signOut() async {} } const _account = AuthAccount( uid: 'u1', provider: AuthProviderKind.email, email: 'fan@fanuzu.co.kr', ); /// 화면 밖에 있으면 먼저 굴려 놓고 누른다. 로그인 화면은 세로로 길다. Future tapVisible(WidgetTester tester, Finder finder) async { await tester.ensureVisible(finder); await tester.pumpAndSettle(); await tester.tap(finder); } void main() { group('별첨 B v1.4 확정 4종', () { test('노출 목록은 Google·Apple·Kakao·LINE 이고 이메일·네이버·X 는 없다', () { final kinds = AuthRepository().providers.map((p) => p.provider).toList(); 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('이메일만 입력 폼을 요구한다', () { for (final kind in AuthProviderKind.values) { expect(kind.needsForm, kind == AuthProviderKind.email); } }); test('QA 이메일 경로는 디버그 빌드에만 있다', () { expect(AuthRepository(releaseMode: false).qaEmailProvider, isNotNull); expect(AuthRepository(releaseMode: true).qaEmailProvider, isNull); }); }); group('저장소 — QA 이메일', () { test('로그인에 성공하면 세션이 바뀐다', () async { final fake = _FakeEmail(result: const AuthSucceeded(_account)); final auth = AuthRepository(qaEmailProvider: fake, releaseMode: false); final outcome = await auth.signInWithEmail( email: 'fan@fanuzu.co.kr', password: 'password12', ); expect(outcome, isA()); expect(auth.isSignedIn, isTrue); expect(fake.lastWasSignUp, isFalse); }); test('가입은 로그인과 다른 경로로 간다', () async { // 한 버튼에 합치면 오타로 친 이메일이 새 계정을 만들어 버린다. final fake = _FakeEmail(result: const AuthSucceeded(_account)); final auth = AuthRepository(qaEmailProvider: fake, releaseMode: false); await auth.signInWithEmail( email: 'fan@fanuzu.co.kr', password: 'password12', signUp: true, ); expect(fake.lastWasSignUp, isTrue); }); test('실패하면 세션이 그대로다', () async { final auth = AuthRepository( qaEmailProvider: _FakeEmail( result: const AuthFailed('E-AUTH-01', '확인해 주세요.'), ), releaseMode: false, ); final outcome = await auth.signInWithEmail(email: 'a@b.c', password: 'x'); expect(outcome, isA()); expect(auth.isSignedIn, isFalse); }); 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()); expect(auth.isSignedIn, isFalse); }); }); group('로그인 화면 — QA 이메일 폼(숨은 입구)', () { Future 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(qaEmailProvider: fake, releaseMode: false); await tester.pumpWidget(MaterialApp(home: LoginScreen(auth: auth))); return auth; } /// 숨은 입구 — 로고를 길게 누른다. 보이는 진입점은 두지 않는다. Future 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 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 openQaForm(tester); await tapVisible(tester, find.widgetWithText(FilledButton, '로그인')); await tester.pump(); expect(find.text('이메일을 입력해 주세요.'), findsOneWidget); expect(fake.lastEmail, isNull); }); testWidgets('이메일 형식이 아니면 막는다', (tester) async { final fake = _FakeEmail(); await pump(tester, fake); await openQaForm(tester); await tester.enterText(find.byType(TextFormField).first, 'fanuzu.co.kr'); await tester.enterText(find.byType(TextFormField).last, 'password12'); await tapVisible(tester, find.widgetWithText(FilledButton, '로그인')); await tester.pump(); expect(find.text('이메일 형식이 올바르지 않습니다.'), findsOneWidget); expect(fake.lastEmail, isNull); }); testWidgets('가입 모드에서는 짧은 비밀번호를 막는다', (tester) async { final fake = _FakeEmail(); await pump(tester, fake); 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).last, 'short'); await tapVisible(tester, find.widgetWithText(FilledButton, '가입하기')); await tester.pump(); expect( find.text('${EmailFormMetrics.minPasswordLength}자 이상으로 정해 주세요.'), findsOneWidget, ); expect(fake.lastEmail, isNull); }); testWidgets('올바른 값이면 제공자를 부른다', (tester) async { final fake = _FakeEmail(result: const AuthSucceeded(_account)); await pump(tester, fake); await openQaForm(tester); 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(); await tester.pump(); expect(fake.lastEmail, 'fan@fanuzu.co.kr'); expect(fake.lastPassword, 'password12'); }); testWidgets('재설정은 이메일이 비어 있으면 보내지 않는다', (tester) async { final fake = _FakeEmail(); await pump(tester, fake); await openQaForm(tester); await tapVisible(tester, find.text('비밀번호 재설정')); await tester.pump(); expect(fake.resetSentTo, isNull); expect(find.textContaining('먼저 입력해'), findsOneWidget); }); testWidgets('재설정 안내는 계정 존재 여부를 드러내지 않는다', (tester) async { final fake = _FakeEmail(); await pump(tester, fake); await openQaForm(tester); await tester.enterText( find.byType(TextFormField).first, 'nobody@fanuzu.co.kr', ); await tapVisible(tester, find.text('비밀번호 재설정')); await tester.pump(); await tester.pump(); expect(fake.resetSentTo, 'nobody@fanuzu.co.kr'); // 「가입되지 않은 이메일」이라고 알려 주면 가입 목록을 훑을 수 있다. expect(find.textContaining('보냈습니다'), findsOneWidget); }); }); }