import 'package:fancake/core/firebase/firebase_boot.dart'; import 'package:fancake/features/auth/auth.dart'; import 'package:fancake/features/auth/auth_repository.dart'; import 'package:fancake/features/auth/firebase_account_linker.dart'; import 'package:fancake/features/auth/social_providers.dart'; import 'package:firebase_auth/firebase_auth.dart' as fb; import 'package:firebase_auth_mocks/firebase_auth_mocks.dart'; import 'package:flutter_test/flutter_test.dart'; /// 카카오·LINE OIDC 제공자 — 콘솔 설정 전이라 실 로그인은 못 하고, /// Firebase 호출 전후 계약(결과 매핑·취소·연결)만 확인한다. void main() { setUp(() => FirebaseBoot.debugSetMode(FirebaseMode.live, reason: 'OIDC 테스트')); tearDown( () => FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: 'OIDC 테스트 끝'), ); OidcSignInProvider kakao(fb.FirebaseAuth auth) => OidcSignInProvider( AuthProviderKind.kakao, providerId: 'oidc.kakao', firebaseAuth: auth, ); test('기본 목록의 카카오·LINE 은 Firebase OIDC 제공자다 — 「준비 중」이 아니다', () { final providers = { for (final p in AuthRepository().providers) p.provider: p, }; for (final kind in [AuthProviderKind.kakao, AuthProviderKind.line]) { final p = providers[kind]; expect(p, isA(), reason: kind.name); expect(p!.isLive, isTrue); expect( (p as OidcSignInProvider).providerId, FirebaseAccountLinker.providerIds[kind], ); expect(p.authProvider.providerId, 'oidc.${kind.name}'); } }); test('Firebase 설정이 없으면 SDK 를 부르지 않고 미설정으로 닫는다', () async { FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: '설정 없음'); final outcome = await kakao(_ThrowingAuth('should-not-call')).signIn(); expect(outcome, isA()); }); test('성공하면 카카오 계정으로 로그인된다', () async { final auth = MockFirebaseAuth(mockUser: MockUser(uid: 'k-1')); final outcome = await kakao(auth).signIn(); expect(outcome, isA()); expect((outcome as AuthSucceeded).account.provider, AuthProviderKind.kakao); expect(outcome.account.uid, 'k-1'); }); test('인가 화면을 닫으면 Android·iOS 철자 모두 취소로 본다', () async { for (final code in ['web-context-canceled', 'web-context-cancelled']) { final outcome = await kakao(_ThrowingAuth(code)).signIn(); expect(outcome, isA(), reason: code); } }); test('콘솔에서 제공자를 안 켰으면 E-AUTH-11', () async { final outcome = await kakao( _ThrowingAuth('operation-not-allowed'), ).signIn(); expect((outcome as AuthFailed).code, 'E-AUTH-11'); }); group('계정 연결', () { test('같은 UID 에 oidc.kakao 를 붙인다', () async { final user = _LinkingUser(linkedEmail: 'fan@fanuzu.co.kr'); final auth = MockFirebaseAuth(signedIn: true, mockUser: user); final linker = FirebaseAccountLinker( providers: [kakao(auth)], firebaseAuth: auth, ); final outcome = await linker.link(AuthProviderKind.kakao); expect(outcome, isA()); expect(linker.linkedAccounts.keys, contains(AuthProviderKind.kakao)); }); test('다른 이메일의 카카오 계정은 붙였다가 바로 떼고 E-AUTH-08', () async { final user = _LinkingUser(linkedEmail: 'someone@else.com'); final auth = MockFirebaseAuth(signedIn: true, mockUser: user); final linker = FirebaseAccountLinker( providers: [kakao(auth)], firebaseAuth: auth, ); final outcome = await linker.link(AuthProviderKind.kakao); expect((outcome as AuthFailed).code, 'E-AUTH-08'); expect( linker.linkedAccounts.keys, isNot(contains(AuthProviderKind.kakao)), ); }); }); } class _ThrowingAuth extends MockFirebaseAuth { _ThrowingAuth(this.code); final String code; @override Future signInWithProvider(fb.AuthProvider provider) => throw fb.FirebaseAuthException(code: code); } /// Google 로 가입한 사용자. 연결하면 [linkedEmail] 을 가진 제공자 정보가 붙는다. // ignore: must_be_immutable class _LinkingUser extends MockUser { _LinkingUser({required this.linkedEmail}) : super( uid: 'u-1', email: 'fan@fanuzu.co.kr', providerData: [_info('google.com', 'fan@fanuzu.co.kr')], ); final String linkedEmail; @override Future linkWithProvider(fb.AuthProvider provider) async { final result = await super.linkWithProvider(provider); providerData ..removeLast() ..add(_info(provider.providerId, linkedEmail)); return result; } } fb.UserInfo _info(String providerId, String email) => // ignore: invalid_use_of_protected_member fb.UserInfo.fromJson({ 'uid': 'p-$providerId', 'email': email, 'providerId': providerId, 'isAnonymous': false, 'isEmailVerified': true, });