소셜 웹 로그인 중단·실패를 이메일 오류 대신 소셜 로그인 문구로 안내 · 네트워크 오류 안내 · Android 중단된 웹 로그인 정리 후 재시도

This commit is contained in:
Sn-Kinos
2026-10-10 21:23:15 +09:00
parent 4d2ce67886
commit bc19797158
7 changed files with 219 additions and 25 deletions
+60
View File
@@ -8,6 +8,7 @@ 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';
@@ -112,6 +113,52 @@ void main() {
expect(outcome, isA<AuthCancelled>());
});
group('웹 흐름 중단 뒤 재시도', () {
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');
@@ -219,6 +266,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