303 lines
8.8 KiB
Dart
303 lines
8.8 KiB
Dart
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<AuthSucceeded>(), reason: provider.name);
|
|
expect(auth.account?.uid, account.uid, reason: provider.name);
|
|
expect(auth.isSignedIn, isTrue, reason: provider.name);
|
|
auth.dispose();
|
|
}
|
|
});
|
|
|
|
test('회원가입 모드 성공만 가입 완료 화면 플래그를 세우고, 시작하면 내린다', () async {
|
|
const account = AuthAccount(uid: 'new', provider: SocialProvider.google);
|
|
final auth = AuthRepository(
|
|
providers: [
|
|
_FakeProvider(
|
|
SocialProvider.google,
|
|
result: const AuthSucceeded(account),
|
|
),
|
|
],
|
|
releaseMode: true,
|
|
);
|
|
await auth.signIn(SocialProvider.google);
|
|
expect(auth.justSignedUp, isFalse);
|
|
await auth.signOut();
|
|
await auth.signIn(SocialProvider.google, signUp: true);
|
|
expect(auth.justSignedUp, isTrue);
|
|
auth.finishSignup();
|
|
expect(auth.justSignedUp, isFalse);
|
|
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<AuthCancelled>(),
|
|
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<AuthFailed>(), 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<AuthNotConfigured>(), 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<AuthFailed>());
|
|
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<void>.delayed(Duration.zero);
|
|
expect(auth.account?.uid, 'apple-user');
|
|
|
|
session.emit(null);
|
|
await Future<void>.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<AuthSucceeded>());
|
|
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<StateError>()));
|
|
|
|
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<String> get missingConfig => const [];
|
|
|
|
@override
|
|
Future<AuthOutcome> signIn() async {
|
|
if (signInError case final error?) throw error;
|
|
return result!;
|
|
}
|
|
|
|
@override
|
|
Future<void> signOut() async {
|
|
signOutCalls += 1;
|
|
if (signOutError case final error?) throw error;
|
|
}
|
|
}
|
|
|
|
final class _FakeSession implements AuthSession {
|
|
_FakeSession(this._current);
|
|
|
|
AuthAccount? _current;
|
|
final StreamController<AuthAccount?> _changes =
|
|
StreamController<AuthAccount?>.broadcast();
|
|
int signOutCalls = 0;
|
|
|
|
@override
|
|
AuthAccount? get currentAccount => _current;
|
|
|
|
@override
|
|
Stream<AuthAccount?> get accountChanges => _changes.stream;
|
|
|
|
void emit(AuthAccount? account) {
|
|
_current = account;
|
|
_changes.add(account);
|
|
}
|
|
|
|
@override
|
|
Future<void> signOut() async {
|
|
signOutCalls += 1;
|
|
_current = null;
|
|
}
|
|
|
|
Future<void> close() => _changes.close();
|
|
}
|