import 'dart:async'; 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/social_providers.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { setUp(() { FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: '인증 저장소 단위 테스트'); }); tearDown(() { debugDefaultTargetPlatformOverride = null; }); group('provider 결과 계약', () { test('4개 provider의 성공 결과가 계정 상태로 반영된다', () async { for (final provider in SocialProvider.values) { final account = AuthAccount( uid: 'uid-${provider.name}', provider: provider, ); final auth = AuthRepository( providers: [_FakeProvider(provider, result: AuthSucceeded(account))], releaseMode: true, ); final outcome = await auth.signIn(provider); expect(outcome, isA(), reason: provider.name); expect(auth.account?.uid, account.uid, reason: provider.name); expect(auth.isSignedIn, isTrue, reason: provider.name); auth.dispose(); } }); test('4개 provider의 취소는 실패 안내나 로그인 상태를 만들지 않는다', () async { for (final provider in SocialProvider.values) { final auth = AuthRepository( providers: [_FakeProvider(provider, result: const AuthCancelled())], releaseMode: true, ); expect( await auth.signIn(provider), isA(), reason: provider.name, ); expect(auth.isSignedIn, isFalse, reason: provider.name); auth.dispose(); } }); test('4개 provider의 SDK 실패가 E-AUTH-03으로 유지된다', () async { for (final provider in SocialProvider.values) { final auth = AuthRepository( providers: [ _FakeProvider( provider, result: const AuthFailed('E-AUTH-03', 'SDK 실패'), ), ], releaseMode: true, ); final outcome = await auth.signIn(provider); expect(outcome, isA(), reason: provider.name); expect( (outcome as AuthFailed).code, 'E-AUTH-03', reason: provider.name, ); expect(auth.isSignedIn, isFalse, reason: provider.name); auth.dispose(); } }); test('4개 provider의 미설정 상태가 provider와 누락 항목을 보존한다', () async { for (final provider in SocialProvider.values) { final auth = AuthRepository( providers: [ _FakeProvider( provider, result: AuthNotConfigured(provider, const ['테스트 설정']), ), ], releaseMode: true, ); final outcome = await auth.signIn(provider); expect(outcome, isA(), reason: provider.name); expect((outcome as AuthNotConfigured).provider, provider); expect(outcome.missing, ['테스트 설정']); expect(auth.isSignedIn, isFalse, reason: provider.name); auth.dispose(); } }); test('문서화되지 않은 SDK 예외도 사용자용 실패로 닫힌다', () async { final auth = AuthRepository( providers: [ _FakeProvider( SocialProvider.google, signInError: StateError('raw SDK error'), ), ], releaseMode: true, ); final outcome = await auth.signIn(SocialProvider.google); expect(outcome, isA()); expect((outcome as AuthFailed).code, 'E-AUTH-03'); expect(outcome.message, '소셜 로그인에 실패했습니다.'); expect(auth.isSignedIn, isFalse); auth.dispose(); }); test('Android Apple 로그인은 웹 인증 설정 전 SDK 호출을 차단한다', () { FirebaseBoot.debugSetMode( FirebaseMode.live, reason: 'Android 설정 게이트 테스트', ); debugDefaultTargetPlatformOverride = TargetPlatform.android; const provider = AppleSignInProvider(); expect(provider.missingConfig, contains('Apple Android 웹 인증 설정')); }); }); group('세션 복원과 로그아웃', () { test('현재 세션을 생성 즉시 복원하고 변경·만료를 계속 반영한다', () async { final initial = const AuthAccount( uid: 'restored-user', provider: SocialProvider.google, ); final session = _FakeSession(initial); final auth = AuthRepository( providers: const [], session: session, releaseMode: true, ); expect(auth.account?.uid, 'restored-user'); expect(auth.isSignedIn, isTrue); session.emit( const AuthAccount(uid: 'apple-user', provider: SocialProvider.apple), ); await Future.delayed(Duration.zero); expect(auth.account?.uid, 'apple-user'); session.emit(null); await Future.delayed(Duration.zero); expect(auth.isSignedIn, isFalse); auth.dispose(); await session.close(); }); test('로그아웃 뒤 같은 provider로 다시 로그인할 수 있다', () async { final provider = _FakeProvider( SocialProvider.google, result: const AuthSucceeded( AuthAccount(uid: 'relogin-user', provider: SocialProvider.google), ), ); final session = _FakeSession( const AuthAccount(uid: 'old-user', provider: SocialProvider.google), ); final auth = AuthRepository( providers: [provider], session: session, releaseMode: true, ); await auth.signOut(); expect(auth.isSignedIn, isFalse); expect(provider.signOutCalls, 1); expect(session.signOutCalls, 1); expect(await auth.signIn(SocialProvider.google), isA()); expect(auth.account?.uid, 'relogin-user'); auth.dispose(); await session.close(); }); test('원격 로그아웃 실패가 나도 로컬 세션은 반드시 정리된다', () async { final provider = _FakeProvider( SocialProvider.apple, result: const AuthCancelled(), signOutError: StateError('provider sign-out failed'), ); final session = _FakeSession( const AuthAccount(uid: 'apple-user', provider: SocialProvider.apple), ); final auth = AuthRepository( providers: [provider], session: session, releaseMode: true, ); await expectLater(auth.signOut(), throwsA(isA())); expect(auth.isSignedIn, isFalse); expect(provider.signOutCalls, 1); expect(session.signOutCalls, 1); auth.dispose(); await session.close(); }); }); } final class _FakeProvider implements SocialAuthProvider { _FakeProvider( this.provider, { this.result, this.signInError, this.signOutError, }); @override final SocialProvider provider; final AuthOutcome? result; final Object? signInError; final Object? signOutError; int signOutCalls = 0; @override bool get isLive => true; @override List get missingConfig => const []; @override Future signIn() async { if (signInError case final error?) throw error; return result!; } @override Future signOut() async { signOutCalls += 1; if (signOutError case final error?) throw error; } } final class _FakeSession implements AuthSession { _FakeSession(this._current); AuthAccount? _current; final StreamController _changes = StreamController.broadcast(); int signOutCalls = 0; @override AuthAccount? get currentAccount => _current; @override Stream get accountChanges => _changes.stream; void emit(AuthAccount? account) { _current = account; _changes.add(account); } @override Future signOut() async { signOutCalls += 1; _current = null; } Future close() => _changes.close(); }