LINE 로그인을 LINE SDK와 서버 인증 토큰 방식으로 변경 — iOS·Android 웹 로그인 중단 문제 해결
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import 'package:cloud_functions/cloud_functions.dart';
|
||||
import 'package:fancake/core/firebase/firebase_boot.dart';
|
||||
import 'package:fancake/features/auth/auth.dart';
|
||||
import 'package:fancake/features/auth/firebase_account_linker.dart';
|
||||
import 'package:fancake/features/auth/social_providers.dart';
|
||||
import 'package:firebase_auth_mocks/firebase_auth_mocks.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// LINE — SDK 로그인(주입) → 서버 교환(주입) → signInWithCustomToken 계약.
|
||||
/// 토큰 검증·기존 oidc.line 사용자 매핑은 서버 테스트(functions/test/auth_line.test.ts)가 본다.
|
||||
void main() {
|
||||
setUp(() => FirebaseBoot.debugSetMode(FirebaseMode.live, reason: 'LINE 테스트'));
|
||||
tearDown(
|
||||
() => FirebaseBoot.debugSetMode(FirebaseMode.mock, reason: 'LINE 테스트 끝'),
|
||||
);
|
||||
|
||||
final sent = <Map<String, dynamic>>[];
|
||||
setUp(sent.clear);
|
||||
|
||||
LineSignInProvider line(
|
||||
MockFirebaseAuth auth, {
|
||||
Future<LineTokens> Function()? login,
|
||||
Object? serverError,
|
||||
String channelId = '2000000001',
|
||||
}) => LineSignInProvider(
|
||||
login:
|
||||
login ?? () async => (idToken: 'line-id', accessToken: 'line-access'),
|
||||
exchange: (payload) async {
|
||||
sent.add(payload);
|
||||
if (serverError != null) throw serverError;
|
||||
return {
|
||||
'token': 'custom-token',
|
||||
if (payload['link'] == true) 'linked': true,
|
||||
};
|
||||
},
|
||||
firebaseAuth: auth,
|
||||
channelId: channelId,
|
||||
);
|
||||
|
||||
test('성공 — LINE 토큰을 서버에 넘기고 커스텀 토큰으로 로그인한다', () async {
|
||||
final auth = MockFirebaseAuth(mockUser: MockUser(uid: 'line:U1'));
|
||||
final outcome = await line(auth).signIn();
|
||||
expect(sent.single, {'idToken': 'line-id', 'accessToken': 'line-access'});
|
||||
expect((outcome as AuthSucceeded).account.uid, 'line:U1');
|
||||
expect(outcome.account.provider, AuthProviderKind.line);
|
||||
expect(auth.currentUser?.uid, 'line:U1');
|
||||
});
|
||||
|
||||
test('ID 토큰이 없으면(openid 미동의) 액세스 토큰만 넘긴다', () async {
|
||||
await line(
|
||||
MockFirebaseAuth(),
|
||||
login: () async => (idToken: null, accessToken: 'line-access'),
|
||||
).signIn();
|
||||
expect(sent.single, {'accessToken': 'line-access'});
|
||||
});
|
||||
|
||||
test('채널 ID 가 없으면 SDK·서버를 부르지 않고 미설정', () async {
|
||||
final outcome = await line(MockFirebaseAuth(), channelId: '').signIn();
|
||||
expect(outcome, isA<AuthNotConfigured>());
|
||||
expect(sent, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'LINE 로그인 취소(Android CANCEL · iOS 3003)는 취소 · 서버를 부르지 않는다 · 다시 시도하면 된다',
|
||||
() async {
|
||||
for (final code in ['CANCEL', '3003']) {
|
||||
final outcome = await line(
|
||||
MockFirebaseAuth(),
|
||||
login: () async => throw PlatformException(code: code),
|
||||
).signIn();
|
||||
expect(outcome, isA<AuthCancelled>(), reason: code);
|
||||
}
|
||||
expect(sent, isEmpty);
|
||||
final retry = await line(
|
||||
MockFirebaseAuth(mockUser: MockUser(uid: 'line:U1')),
|
||||
).signIn();
|
||||
expect(retry, isA<AuthSucceeded>());
|
||||
},
|
||||
);
|
||||
|
||||
test('SDK 실패는 소셜 문구 E-AUTH-03', () async {
|
||||
final outcome = await line(
|
||||
MockFirebaseAuth(),
|
||||
login: () async =>
|
||||
throw PlatformException(code: 'AUTHENTICATION_AGENT_ERROR'),
|
||||
).signIn();
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-03');
|
||||
expect(outcome.message, '소셜 로그인에 실패했습니다.');
|
||||
expect(sent, isEmpty);
|
||||
});
|
||||
|
||||
test('서버 오류는 카탈로그 코드로 · 로그인하지 않는다', () async {
|
||||
final auth = MockFirebaseAuth();
|
||||
final outcome = await line(
|
||||
auth,
|
||||
serverError: FirebaseFunctionsException(
|
||||
code: 'permission-denied',
|
||||
message: 'LINE 인증을 확인하지 못했습니다.',
|
||||
details: {'code': 'E-AUTH-03'},
|
||||
),
|
||||
).signIn();
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-03');
|
||||
expect(outcome.message, '소셜 로그인에 실패했습니다.');
|
||||
expect(auth.currentUser, isNull);
|
||||
});
|
||||
|
||||
group('SNS 연동', () {
|
||||
test('로그인한 사용자에 link: true 로 붙인다', () async {
|
||||
final auth = MockFirebaseAuth(
|
||||
signedIn: true,
|
||||
mockUser: MockUser(uid: 'u-1'),
|
||||
);
|
||||
final linker = FirebaseAccountLinker(
|
||||
providers: [line(auth)],
|
||||
firebaseAuth: auth,
|
||||
);
|
||||
final outcome = await linker.link(AuthProviderKind.line);
|
||||
expect(sent.single, {
|
||||
'idToken': 'line-id',
|
||||
'accessToken': 'line-access',
|
||||
'link': true,
|
||||
});
|
||||
expect((outcome as AuthSucceeded).account.uid, 'u-1');
|
||||
});
|
||||
|
||||
test('연결 중 취소는 취소 · 서버를 부르지 않는다', () async {
|
||||
final auth = MockFirebaseAuth(
|
||||
signedIn: true,
|
||||
mockUser: MockUser(uid: 'u-1'),
|
||||
);
|
||||
final outcome = await line(
|
||||
auth,
|
||||
login: () async => throw PlatformException(code: 'CANCEL'),
|
||||
).link(auth.currentUser!);
|
||||
expect(outcome, isA<AuthCancelled>());
|
||||
expect(sent, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'다른 계정에 붙은 LINE 이면 E-AUTH-08 · 이미 다른 LINE 이면 E-AUTH-09 · 세션 만료면 다시 로그인 안내',
|
||||
() async {
|
||||
final auth = MockFirebaseAuth(
|
||||
signedIn: true,
|
||||
mockUser: MockUser(uid: 'u-1'),
|
||||
);
|
||||
Future<AuthOutcome> failWith(Map<String, String> details) => line(
|
||||
auth,
|
||||
serverError: FirebaseFunctionsException(
|
||||
code: 'x',
|
||||
message: 'x',
|
||||
details: details,
|
||||
),
|
||||
).link(auth.currentUser!);
|
||||
|
||||
final inUse = await failWith({'code': 'E-AUTH-08'});
|
||||
expect((inUse as AuthFailed).code, 'E-AUTH-08');
|
||||
expect(inUse.message, '다른 FANUZU 계정에 이미 연결된 소셜 계정입니다.');
|
||||
expect(
|
||||
((await failWith({'code': 'E-AUTH-09'})) as AuthFailed).code,
|
||||
'E-AUTH-09',
|
||||
);
|
||||
final expired = await failWith({
|
||||
'code': 'E-AUTH-03',
|
||||
'reason': 'session',
|
||||
});
|
||||
expect((expired as AuthFailed).message, '보안을 위해 다시 로그인한 뒤 시도해 주세요.');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -12,7 +12,7 @@ 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).
|
||||
@@ -28,18 +28,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>());
|
||||
});
|
||||
|
||||
@@ -87,24 +80,6 @@ void main() {
|
||||
expect((outcome as AuthFailed).code, 'E-AUTH-11');
|
||||
});
|
||||
|
||||
test('LINE 은 Android · iOS 모두 앱 자동 로그인을 끈다', () {
|
||||
final line =
|
||||
AuthRepository().providers.firstWhere(
|
||||
(p) => p.provider == AuthProviderKind.line,
|
||||
)
|
||||
as OidcSignInProvider;
|
||||
expect(line.authProvider.parameters, {
|
||||
'disable_auto_login': 'true',
|
||||
'disable_ios_auto_login': 'true',
|
||||
});
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
||||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||
expect(line.authProvider.parameters, {
|
||||
'disable_auto_login': 'true',
|
||||
'disable_ios_auto_login': 'true',
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('Android: 앱으로 돌아왔는데 결과가 없으면 취소로 풀린다', (tester) async {
|
||||
late AuthOutcome outcome;
|
||||
line(_HangingAuth()).signIn().then((o) => outcome = o);
|
||||
|
||||
Reference in New Issue
Block a user