1차 보완(0.1.1+14) 내용 합치기
This commit is contained in:
+138
-31
@@ -7,11 +7,11 @@ 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/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// LINE OIDC 제공자 — 실 로그인은 못 하고,
|
||||
/// Firebase 웹 흐름(OIDC) 제공자 — 지금은 Android Apple 이 쓴다(LINE 을 예시 제공자로 둔다). 실 로그인은 못 하고,
|
||||
/// Firebase 호출 전후 계약(결과 매핑·취소·연결)만 확인한다.
|
||||
void main() {
|
||||
// Android 에서 signIn 이 앱 생명주기를 듣는다(untilAbandoned).
|
||||
@@ -27,18 +27,11 @@ void main() {
|
||||
firebaseAuth: auth,
|
||||
);
|
||||
|
||||
test('기본 목록: LINE 은 Firebase OIDC, 카카오는 SDK + 서버 커스텀 토큰', () {
|
||||
test('기본 목록: 카카오 · LINE 은 SDK + 서버 커스텀 토큰', () {
|
||||
final providers = {
|
||||
for (final p in AuthRepository().providers) p.provider: p,
|
||||
};
|
||||
final p = providers[AuthProviderKind.line];
|
||||
expect(p, isA<OidcSignInProvider>());
|
||||
expect(p!.isLive, isTrue);
|
||||
expect(
|
||||
(p as OidcSignInProvider).providerId,
|
||||
FirebaseAccountLinker.providerIds[AuthProviderKind.line],
|
||||
);
|
||||
expect(p.authProvider.providerId, 'oidc.line');
|
||||
expect(providers[AuthProviderKind.line], isA<LineSignInProvider>());
|
||||
expect(providers[AuthProviderKind.kakao], isA<KakaoSignInProvider>());
|
||||
});
|
||||
|
||||
@@ -68,18 +61,6 @@ void main() {
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-11');
|
||||
});
|
||||
|
||||
test('Android 의 LINE 은 앱 자동 로그인을 끈다 · iOS 는 파라미터 없음', () {
|
||||
final line =
|
||||
AuthRepository().providers.firstWhere(
|
||||
(p) => p.provider == AuthProviderKind.line,
|
||||
)
|
||||
as OidcSignInProvider;
|
||||
expect(line.authProvider.parameters, {'disable_auto_login': 'true'});
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||
expect(line.authProvider.parameters, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Android: 앱으로 돌아왔는데 결과가 없으면 취소로 풀린다', (tester) async {
|
||||
late AuthOutcome outcome;
|
||||
line(_HangingAuth()).signIn().then((o) => outcome = o);
|
||||
@@ -94,6 +75,52 @@ void main() {
|
||||
expect(outcome, isA<AuthCancelled>());
|
||||
});
|
||||
|
||||
group('웹 흐름 중단 뒤 재시도 (10/8 실기기 LINE · R01)', () {
|
||||
late List<String> cleared;
|
||||
setUp(() {
|
||||
cleared = [];
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(const MethodChannel('kr.co.fanuzu.app/auth'), (call) async {
|
||||
cleared.add(call.method);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
tearDown(() => TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(const MethodChannel('kr.co.fanuzu.app/auth'), null));
|
||||
|
||||
test('앞선 흐름이 남아 막히면(web-context-already-presented) 정리하고 한 번 다시 열어 로그인된다', () async {
|
||||
final auth = _StaleThenOkAuth();
|
||||
final outcome = await line(auth).signIn();
|
||||
expect(outcome, isA<AuthSucceeded>());
|
||||
expect(auth.calls, 2);
|
||||
expect(cleared, ['cancelPendingWebSignIn']);
|
||||
});
|
||||
|
||||
test('정리 뒤에도 막히면 소셜 문구로 실패 — 「이메일 또는 비밀번호」가 아니다', () async {
|
||||
final outcome = await line(_ThrowingAuth('web-context-already-presented')).signIn();
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-03');
|
||||
expect(outcome.message, '로그인이 완료되지 않았습니다. 다시 시도해 주세요.');
|
||||
});
|
||||
|
||||
test('망 오류도 소셜 경로 문구', () async {
|
||||
final outcome = await line(_ThrowingAuth('web-network-request-failed')).signIn();
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-03');
|
||||
expect(outcome.message, '네트워크 연결을 확인한 뒤 다시 시도해 주세요.');
|
||||
});
|
||||
|
||||
testWidgets('Android: 결과 없이 돌아와 취소로 풀릴 때 Firebase 대기도 정리한다', (tester) async {
|
||||
late AuthOutcome outcome;
|
||||
line(_HangingAuth()).signIn().then((o) => outcome = o);
|
||||
for (final s in [AppLifecycleState.inactive, AppLifecycleState.hidden, AppLifecycleState.paused,
|
||||
AppLifecycleState.hidden, AppLifecycleState.inactive, AppLifecycleState.resumed]) {
|
||||
tester.binding.handleAppLifecycleStateChanged(s);
|
||||
}
|
||||
await tester.pump(const Duration(seconds: 9));
|
||||
expect(outcome, isA<AuthCancelled>());
|
||||
expect(cleared, ['cancelPendingWebSignIn']);
|
||||
});
|
||||
});
|
||||
|
||||
group('계정 연결', () {
|
||||
test('같은 UID 에 oidc.line 을 붙인다', () async {
|
||||
final user = _LinkingUser(linkedEmail: 'fan@fanuzu.co.kr');
|
||||
@@ -109,8 +136,27 @@ void main() {
|
||||
expect(linker.linkedAccounts.keys, contains(AuthProviderKind.line));
|
||||
});
|
||||
|
||||
test('다른 이메일의 LINE 계정은 붙였다가 바로 떼고 E-AUTH-08', () async {
|
||||
final user = _LinkingUser(linkedEmail: 'someone@else.com');
|
||||
test('이메일이 다르거나 없는 LINE 계정도 붙는다 — 이메일 일치는 소유 확인이 아니다(Q-E8 · R01)', () async {
|
||||
for (final email in ['someone@else.com', null]) {
|
||||
final user = _LinkingUser(linkedEmail: email);
|
||||
final auth = MockFirebaseAuth(signedIn: true, mockUser: user);
|
||||
final linker = FirebaseAccountLinker(
|
||||
providers: [line(auth)],
|
||||
firebaseAuth: auth,
|
||||
);
|
||||
|
||||
final outcome = await linker.link(AuthProviderKind.line);
|
||||
|
||||
expect(outcome, isA<AuthSucceeded>(), reason: '$email');
|
||||
expect(linker.linkedAccounts.keys, contains(AuthProviderKind.line));
|
||||
}
|
||||
});
|
||||
|
||||
test('다른 FANUZU 계정에 붙은 LINE 은 Firebase 거부를 E-AUTH-08 로 · 연결되지 않는다', () async {
|
||||
final user = _LinkingUser(
|
||||
linkedEmail: null,
|
||||
error: fb.FirebaseAuthException(code: 'credential-already-in-use'),
|
||||
);
|
||||
final auth = MockFirebaseAuth(signedIn: true, mockUser: user);
|
||||
final linker = FirebaseAccountLinker(
|
||||
providers: [line(auth)],
|
||||
@@ -120,14 +166,60 @@ void main() {
|
||||
final outcome = await linker.link(AuthProviderKind.line);
|
||||
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-08');
|
||||
expect(
|
||||
linker.linkedAccounts.keys,
|
||||
isNot(contains(AuthProviderKind.line)),
|
||||
expect(outcome.message, '다른 FANUZU 계정에 이미 연결된 소셜 계정입니다.');
|
||||
expect(linker.linkedAccounts.keys, isNot(contains(AuthProviderKind.line)));
|
||||
});
|
||||
|
||||
test('Google · Apple(자격증명 경로)도 이메일이 달라도 붙는다(Q-E8)', () async {
|
||||
final auth = MockFirebaseAuth(
|
||||
signedIn: true,
|
||||
mockUser: MockUser(uid: 'u-1', email: 'fan@fanuzu.co.kr'),
|
||||
);
|
||||
final outcome = await FirebaseAccountLinker(
|
||||
providers: [const _FakeCredential('other@gmail.com')],
|
||||
firebaseAuth: auth,
|
||||
).link(AuthProviderKind.google);
|
||||
expect(outcome, isA<AuthSucceeded>());
|
||||
});
|
||||
|
||||
test('세션이 만료됐으면 다시 로그인 안내(E-AUTH-12)', () async {
|
||||
final user = _LinkingUser(
|
||||
linkedEmail: null,
|
||||
error: fb.FirebaseAuthException(code: 'user-token-expired'),
|
||||
);
|
||||
final auth = MockFirebaseAuth(signedIn: true, mockUser: user);
|
||||
final outcome = await FirebaseAccountLinker(
|
||||
providers: [line(auth)],
|
||||
firebaseAuth: auth,
|
||||
).link(AuthProviderKind.line);
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-12');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 이메일을 함께 주는 자격증명 제공자(Google 흉내).
|
||||
final class _FakeCredential implements SocialAuthProvider, CredentialProvider {
|
||||
const _FakeCredential(this.email);
|
||||
final String email;
|
||||
|
||||
@override
|
||||
AuthProviderKind get provider => AuthProviderKind.google;
|
||||
@override
|
||||
bool get isLive => true;
|
||||
@override
|
||||
List<String> get missingConfig => const [];
|
||||
@override
|
||||
Future<AuthOutcome> signIn() async => const AuthCancelled();
|
||||
@override
|
||||
Future<void> signOut() async {}
|
||||
@override
|
||||
Future<SocialCredential?> obtainCredential() async => (
|
||||
credential: fb.GoogleAuthProvider.credential(idToken: 'id'),
|
||||
email: email,
|
||||
name: null,
|
||||
);
|
||||
}
|
||||
|
||||
class _ThrowingAuth extends MockFirebaseAuth {
|
||||
_ThrowingAuth(this.code);
|
||||
final String code;
|
||||
@@ -137,6 +229,19 @@ class _ThrowingAuth extends MockFirebaseAuth {
|
||||
throw fb.FirebaseAuthException(code: code);
|
||||
}
|
||||
|
||||
/// 앞선 웹 흐름이 남아 첫 시도가 막히는 Firebase(Android `web-context-already-presented`).
|
||||
class _StaleThenOkAuth extends MockFirebaseAuth {
|
||||
_StaleThenOkAuth() : super(mockUser: MockUser(uid: 'line-1'));
|
||||
var calls = 0;
|
||||
|
||||
@override
|
||||
Future<fb.UserCredential> signInWithProvider(fb.AuthProvider provider) {
|
||||
calls += 1;
|
||||
if (calls == 1) throw fb.FirebaseAuthException(code: 'web-context-already-presented');
|
||||
return super.signInWithProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
/// 콜백을 잃어 영영 끝나지 않는 Firebase 대기 Task.
|
||||
class _HangingAuth extends MockFirebaseAuth {
|
||||
@override
|
||||
@@ -147,17 +252,19 @@ class _HangingAuth extends MockFirebaseAuth {
|
||||
/// Google 로 가입한 사용자. 연결하면 [linkedEmail] 을 가진 제공자 정보가 붙는다.
|
||||
// ignore: must_be_immutable
|
||||
class _LinkingUser extends MockUser {
|
||||
_LinkingUser({required this.linkedEmail})
|
||||
_LinkingUser({required this.linkedEmail, this.error})
|
||||
: super(
|
||||
uid: 'u-1',
|
||||
email: 'fan@fanuzu.co.kr',
|
||||
providerData: [_info('google.com', 'fan@fanuzu.co.kr')],
|
||||
);
|
||||
|
||||
final String linkedEmail;
|
||||
final String? linkedEmail;
|
||||
final fb.FirebaseAuthException? error;
|
||||
|
||||
@override
|
||||
Future<fb.UserCredential> linkWithProvider(fb.AuthProvider provider) async {
|
||||
if (error case final e?) throw e;
|
||||
final result = await super.linkWithProvider(provider);
|
||||
providerData
|
||||
..removeLast()
|
||||
@@ -166,7 +273,7 @@ class _LinkingUser extends MockUser {
|
||||
}
|
||||
}
|
||||
|
||||
fb.UserInfo _info(String providerId, String email) =>
|
||||
fb.UserInfo _info(String providerId, String? email) =>
|
||||
// ignore: invalid_use_of_protected_member
|
||||
fb.UserInfo.fromJson({
|
||||
'uid': 'p-$providerId',
|
||||
|
||||
Reference in New Issue
Block a user