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 = >[]; setUp(sent.clear); LineSignInProvider line( MockFirebaseAuth auth, { Future 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()); 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(), reason: code); } expect(sent, isEmpty); final retry = await line( MockFirebaseAuth(mockUser: MockUser(uid: 'line:U1')), ).signIn(); expect(retry, isA()); }, ); 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()); expect(sent, isEmpty); }); test( '다른 계정에 붙은 LINE 이면 E-AUTH-08 · 이미 다른 LINE 이면 E-AUTH-09 · 세션 만료면 다시 로그인 안내', () async { final auth = MockFirebaseAuth( signedIn: true, mockUser: MockUser(uid: 'u-1'), ); Future failWith(Map 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, '보안을 위해 다시 로그인한 뒤 시도해 주세요.'); }, ); }); }