Kakao 로그인(카카오 SDK · 서버 커스텀 토큰)과 LINE 로그인(Firebase OIDC) 연결 · SNS 연동 · 인증 취소 처리
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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/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/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// LINE OIDC 제공자 — 실 로그인은 못 하고,
|
||||
/// Firebase 호출 전후 계약(결과 매핑·취소·연결)만 확인한다.
|
||||
void main() {
|
||||
// Android 에서 signIn 이 앱 생명주기를 듣는다(untilAbandoned).
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUp(() => FirebaseBoot.debugSetMode(FirebaseMode.live, reason: 'OIDC 테스트'));
|
||||
tearDown(
|
||||
() => FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: 'OIDC 테스트 끝'),
|
||||
);
|
||||
|
||||
OidcSignInProvider line(fb.FirebaseAuth auth) => OidcSignInProvider(
|
||||
AuthProviderKind.line,
|
||||
providerId: 'oidc.line',
|
||||
firebaseAuth: auth,
|
||||
);
|
||||
|
||||
test('기본 목록: LINE 은 Firebase OIDC, 카카오는 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.kakao], isA<KakaoSignInProvider>());
|
||||
});
|
||||
|
||||
test('Firebase 설정이 없으면 SDK 를 부르지 않고 미설정으로 닫는다', () async {
|
||||
FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: '설정 없음');
|
||||
final outcome = await line(_ThrowingAuth('should-not-call')).signIn();
|
||||
expect(outcome, isA<AuthNotConfigured>());
|
||||
});
|
||||
|
||||
test('성공하면 LINE 계정으로 로그인된다', () async {
|
||||
final auth = MockFirebaseAuth(mockUser: MockUser(uid: 'k-1'));
|
||||
final outcome = await line(auth).signIn();
|
||||
expect(outcome, isA<AuthSucceeded>());
|
||||
expect((outcome as AuthSucceeded).account.provider, AuthProviderKind.line);
|
||||
expect(outcome.account.uid, 'k-1');
|
||||
});
|
||||
|
||||
test('인가 화면을 닫으면 Android·iOS 철자 모두 취소로 본다', () async {
|
||||
for (final code in ['web-context-canceled', 'web-context-cancelled']) {
|
||||
final outcome = await line(_ThrowingAuth(code)).signIn();
|
||||
expect(outcome, isA<AuthCancelled>(), reason: code);
|
||||
}
|
||||
});
|
||||
|
||||
test('콘솔에서 제공자를 안 켰으면 E-AUTH-11', () async {
|
||||
final outcome = await line(_ThrowingAuth('operation-not-allowed')).signIn();
|
||||
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);
|
||||
void go(List<AppLifecycleState> states) =>
|
||||
states.forEach(tester.binding.handleAppLifecycleStateChanged);
|
||||
const inactive = AppLifecycleState.inactive;
|
||||
const hidden = AppLifecycleState.hidden;
|
||||
go([inactive, hidden, AppLifecycleState.paused]); // Custom Tab 으로 나감
|
||||
await tester.pump(const Duration(minutes: 1)); // 브라우저에 오래 있어도 그대로
|
||||
go([hidden, inactive, AppLifecycleState.resumed]); // 앱으로 돌아옴
|
||||
await tester.pump(const Duration(seconds: 9));
|
||||
expect(outcome, isA<AuthCancelled>());
|
||||
});
|
||||
|
||||
group('계정 연결', () {
|
||||
test('같은 UID 에 oidc.line 을 붙인다', () async {
|
||||
final user = _LinkingUser(linkedEmail: 'fan@fanuzu.co.kr');
|
||||
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>());
|
||||
expect(linker.linkedAccounts.keys, contains(AuthProviderKind.line));
|
||||
});
|
||||
|
||||
test('다른 이메일의 LINE 계정은 붙였다가 바로 떼고 E-AUTH-08', () async {
|
||||
final user = _LinkingUser(linkedEmail: 'someone@else.com');
|
||||
final auth = MockFirebaseAuth(signedIn: true, mockUser: user);
|
||||
final linker = FirebaseAccountLinker(
|
||||
providers: [line(auth)],
|
||||
firebaseAuth: auth,
|
||||
);
|
||||
|
||||
final outcome = await linker.link(AuthProviderKind.line);
|
||||
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-08');
|
||||
expect(
|
||||
linker.linkedAccounts.keys,
|
||||
isNot(contains(AuthProviderKind.line)),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _ThrowingAuth extends MockFirebaseAuth {
|
||||
_ThrowingAuth(this.code);
|
||||
final String code;
|
||||
|
||||
@override
|
||||
Future<fb.UserCredential> signInWithProvider(fb.AuthProvider provider) =>
|
||||
throw fb.FirebaseAuthException(code: code);
|
||||
}
|
||||
|
||||
/// 콜백을 잃어 영영 끝나지 않는 Firebase 대기 Task.
|
||||
class _HangingAuth extends MockFirebaseAuth {
|
||||
@override
|
||||
Future<fb.UserCredential> signInWithProvider(fb.AuthProvider provider) =>
|
||||
Completer<fb.UserCredential>().future;
|
||||
}
|
||||
|
||||
/// 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<fb.UserCredential> 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,
|
||||
});
|
||||
Reference in New Issue
Block a user