SNS 계정 연결의 이메일 일치 조건 제거 — 현재 로그인과 새 제공자 인증을 서버에서 확인 · 다른 계정에 연결된 제공자는 거부 · 세션 만료 시 재로그인 안내

This commit is contained in:
Sn-Kinos
2026-10-10 21:14:49 +09:00
parent b66863a149
commit 4d2ce67886
8 changed files with 175 additions and 81 deletions
+74 -8
View File
@@ -127,8 +127,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 계정도 붙는다 — 이메일 일치는 소유 확인이 아니다', () 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)],
@@ -138,14 +157,59 @@ 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(자격증명 경로)도 이메일이 달라도 붙는다', () 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,
);
}
class _ThrowingAuth extends MockFirebaseAuth {
_ThrowingAuth(this.code);
final String code;
@@ -165,17 +229,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()
@@ -184,7 +250,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',