import 'package:fancake/features/auth/auth.dart'; import 'package:fancake/features/auth/login_screen.dart' show authNotice; import 'package:fancake/features/auth/social_providers.dart'; import 'package:firebase_auth/firebase_auth.dart' as fb; import 'package:flutter_test/flutter_test.dart'; /// 인증 오류 문구가 **경로별로** 맞는 사실을 말하는지 고정한다. /// /// `account-exists-with-different-credential` 은 로그인 경로와 계정 연결 경로에서 /// 서로 다른 사실을 뜻한다(별첨 B v1.4 「복수 소셜 Provider 연결 허용」). /// 한 벌로 합쳐 두면 로그인에 실패한 사용자가 자기가 하지도 않은 계정 연결을 /// 설명받고 무엇을 해야 하는지 알 수 없으므로, 회귀 가드를 둔다. void main() { group('account-exists-with-different-credential — 경로별 문구', () { test('로그인 경로는 「다른 수단으로 로그인한 뒤 연결」을 안내한다', () { final message = authMessage( _exception('account-exists-with-different-credential', email: 'fan@gmail.com'), flow: AuthFlow.signIn, ); // 오류 응답의 이메일 힌트를 그대로 넣어야 어느 계정인지 알 수 있다. // 이메일에 조사를 바로 붙이지 않는다 — 도메인 끝 글자에 따라 은/는 이 갈린다. expect(message, startsWith('fan@gmail.com 계정은')); expect(message, contains('처음 가입에 쓰신 수단으로 로그인')); expect(message, contains('SNS 연동')); // 로그인 경로에서는 「연결된 소셜 계정」이 사실이 아니다. expect(message, isNot(contains('이미 연결된 소셜 계정'))); }); test('계정 연결 경로는 「다른 FANUZU 계정에 붙어 있다」로 남는다', () { final message = authMessage( _exception('account-exists-with-different-credential', email: 'fan@gmail.com'), flow: AuthFlow.link, ); expect(message, '다른 FANUZU 계정에 이미 연결된 소셜 계정입니다.'); }); test('두 경로의 문구가 서로 다르다', () { final error = _exception('account-exists-with-different-credential', email: 'fan@gmail.com'); expect( authMessage(error, flow: AuthFlow.signIn), isNot(authMessage(error, flow: AuthFlow.link)), ); }); test('이메일 힌트가 없으면 이메일 없이 같은 안내를 한다', () { // Apple 은 최초 인가 때만 이메일을 준다 — 두 번째부터 힌트가 없다. final message = authMessage( _exception('account-exists-with-different-credential'), flow: AuthFlow.signIn, ); expect(message, startsWith('이 계정은')); expect(message, contains('처음 가입에 쓰신 수단으로 로그인')); expect(message, isNot(contains('null'))); }); test('에러 카탈로그 ID 는 경로와 무관하게 E-AUTH-08 이다', () { // 문구만 갈랐다. 카탈로그를 손대면 Admin·QA 의 코드 대조가 어긋난다. expect( authCatalogCode( _exception('account-exists-with-different-credential'), flow: AuthFlow.signIn, ), 'E-AUTH-08', ); }); }); group('경로와 무관한 코드는 한 문구를 유지한다', () { test('credential-already-in-use 는 양쪽이 같다', () { final error = _exception('credential-already-in-use'); expect( authMessage(error, flow: AuthFlow.signIn), '다른 FANUZU 계정에 이미 연결된 소셜 계정입니다.', ); expect( authMessage(error, flow: AuthFlow.link), authMessage(error, flow: AuthFlow.signIn), ); }); test('계정 존재 여부를 갈라 주지 않는다 — 열거 방지', () { // 「등록되지 않은 이메일」과 「비밀번호 오류」가 같은 문구로 나가야 한다. for (final code in ['user-not-found', 'wrong-password']) { expect( authMessage(_exception(code), flow: AuthFlow.email), '이메일 또는 비밀번호를 확인해 주세요.', reason: code, ); } }); }); group('소셜 웹 로그인 실패 · 중단 — 이메일 문구를 쓰지 않는다(10/8 실기기 LINE · R01)', () { const webCodes = [ 'web-context-already-presented', 'web-network-request-failed', 'web-internal-error', 'internal-error', 'invalid-credential', ]; test('소셜 로그인 · 연결의 알 수 없는 실패는 E-AUTH-03 · 소셜 문구', () { for (final code in webCodes) { for (final flow in [AuthFlow.signIn, AuthFlow.link]) { expect(authCatalogCode(_exception(code), flow: flow), 'E-AUTH-03', reason: '$code $flow'); expect( authMessage(_exception(code), flow: flow), isNot('이메일 또는 비밀번호를 확인해 주세요.'), reason: '$code $flow', ); } } expect( authMessage(_exception('web-context-already-presented'), flow: AuthFlow.signIn), '로그인이 완료되지 않았습니다. 다시 시도해 주세요.', ); expect( authMessage(_exception('web-network-request-failed'), flow: AuthFlow.signIn), '네트워크 연결을 확인한 뒤 다시 시도해 주세요.', ); expect(authMessage(_exception('internal-error'), flow: AuthFlow.signIn), '소셜 로그인에 실패했습니다.'); }); test('이메일 폼만 E-AUTH-01 · 「이메일 또는 비밀번호」', () { expect(authCatalogCode(_exception('internal-error'), flow: AuthFlow.email), 'E-AUTH-01'); expect(authMessage(_exception('internal-error'), flow: AuthFlow.email), '이메일 또는 비밀번호를 확인해 주세요.'); }); test('취소는 성공이 아니고 다시 시도하라고 안내한다', () { expect(authNotice(const AuthCancelled()), '로그인이 완료되지 않았습니다. 다시 시도해 주세요.'); }); }); // 10/4 실기기 2-3 — Android Apple 로그인 다이얼로그에 「발주사 키」 · 설정 항목이 떴다. test('설정 전 제공자 안내는 내부 설정 항목을 내보내지 않는다', () { final notice = authNotice( const AuthNotConfigured(AuthProviderKind.apple, ['Apple Android 웹 인증 설정']), )!; expect(notice, contains('Apple')); expect(notice, isNot(contains('웹 인증 설정'))); expect(notice, isNot(contains('발주사'))); }); } /// `FirebaseAuthException` 의 생성자가 `@protected` 라 밖에서 직접 부르면 /// 분석 경고가 난다. 하위 클래스에서 부르는 것은 허용되므로 테스트용으로 뚫는다. final class _FakeAuthException extends fb.FirebaseAuthException { _FakeAuthException({required super.code, super.email}); } fb.FirebaseAuthException _exception(String code, {String? email}) => _FakeAuthException(code: code, email: email);