Kakao 로그인(카카오 SDK · 서버 커스텀 토큰)과 LINE 로그인(Firebase OIDC) 연결 · SNS 연동 · 인증 취소 처리
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getAuth } from 'firebase-admin/auth';
|
||||
|
||||
import { bootAdmin, shutdownAdmin } from './support.js';
|
||||
import { fnAuthKakao, KAKAO_PROVIDER_ID, kakaoUidOf } from '../src/account/kakao.js';
|
||||
|
||||
/** 카카오 커스텀 토큰 — 토큰 검증 · 기존 oidc.kakao 사용자 매핑 · 신규 UID · SNS 연동. */
|
||||
|
||||
const APP_ID = '123456';
|
||||
const KAKAO_ID = '987654321';
|
||||
const OLD_UID = 'test-kakao-oidc-user';
|
||||
const GOOGLE_UID = 'test-kakao-google-user';
|
||||
|
||||
type Req = Parameters<typeof fnAuthKakao.run>[0];
|
||||
const call = (data: Record<string, unknown>, uid?: string) =>
|
||||
fnAuthKakao.run({ data, ...(uid ? { auth: { uid, token: {} } } : {}) } as Req);
|
||||
const codeOf = async (p: Promise<unknown>) => {
|
||||
try { await p; return 'ok'; } catch (e) {
|
||||
const d = (e as { details?: { code?: string; reason?: string } }).details;
|
||||
return d?.reason ?? d?.code;
|
||||
}
|
||||
};
|
||||
|
||||
/** 카카오 API 흉내. 토큰 'good' 만 우리 앱 것, 'other-app' 은 남의 앱 토큰이다. */
|
||||
function stubKakao(email?: string) {
|
||||
vi.stubGlobal('fetch', vi.fn(async (url: string, init: { headers: Record<string, string> }) => {
|
||||
const token = init.headers.Authorization?.replace('Bearer ', '');
|
||||
if (token === 'expired') return new Response('{}', { status: 401 });
|
||||
if (url.endsWith('/v1/user/access_token_info')) {
|
||||
return Response.json({ id: Number(KAKAO_ID), expires_in: 100, app_id: token === 'other-app' ? 1 : Number(APP_ID) });
|
||||
}
|
||||
return Response.json({
|
||||
id: Number(KAKAO_ID),
|
||||
kakao_account: {
|
||||
profile: { nickname: '카카오팬' },
|
||||
...(email ? { email, is_email_valid: true, is_email_verified: true } : {}),
|
||||
},
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
async function wipe() {
|
||||
const uids = [OLD_UID, GOOGLE_UID, kakaoUidOf(KAKAO_ID)];
|
||||
await getAuth().deleteUsers(uids);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
if (!process.env.FIREBASE_AUTH_EMULATOR_HOST) {
|
||||
throw new Error('FIREBASE_AUTH_EMULATOR_HOST 가 없습니다. `npm run test:emulator` 로 실행하십시오.');
|
||||
}
|
||||
process.env.KAKAO_APP_ID = APP_ID;
|
||||
bootAdmin();
|
||||
});
|
||||
afterAll(async () => { await wipe(); await shutdownAdmin(); });
|
||||
beforeEach(async () => { await wipe(); stubKakao(); });
|
||||
afterEach(() => { vi.unstubAllGlobals(); });
|
||||
|
||||
describe('토큰 검증', () => {
|
||||
it('만료·무효 토큰과 남의 앱 토큰은 E-AUTH-03', async () => {
|
||||
expect(await codeOf(call({ accessToken: 'expired' }))).toBe('E-AUTH-03');
|
||||
expect(await codeOf(call({ accessToken: 'other-app' }))).toBe('E-AUTH-03');
|
||||
});
|
||||
});
|
||||
|
||||
describe('로그인', () => {
|
||||
it('OIDC 시절 oidc.kakao 가입자는 같은 UID 로 들어온다', async () => {
|
||||
await getAuth().createUser({ uid: OLD_UID });
|
||||
await getAuth().updateUser(OLD_UID, { providerToLink: { providerId: KAKAO_PROVIDER_ID, uid: KAKAO_ID } });
|
||||
const { token } = await call({ accessToken: 'good' }) as { token: string };
|
||||
expect(JSON.parse(Buffer.from(token.split('.')[1]!, 'base64url').toString()).uid).toBe(OLD_UID);
|
||||
});
|
||||
|
||||
it('신규는 kakao:<회원번호> 로 만들고 oidc.kakao 를 붙인다 · 두 번째도 같은 UID', async () => {
|
||||
const first = await call({ accessToken: 'good' }) as { token: string };
|
||||
await call({ accessToken: 'good' });
|
||||
const uid = JSON.parse(Buffer.from(first.token.split('.')[1]!, 'base64url').toString()).uid;
|
||||
expect(uid).toBe(kakaoUidOf(KAKAO_ID));
|
||||
const user = await getAuth().getUser(uid);
|
||||
expect(user.displayName).toBe('카카오팬');
|
||||
expect(user.providerData.map((d) => [d.providerId, d.uid])).toEqual([[KAKAO_PROVIDER_ID, KAKAO_ID]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SNS 연동', () => {
|
||||
it('로그인 없이 link 는 E-AUTH-03', async () => {
|
||||
expect(await codeOf(call({ accessToken: 'good', link: true }))).toBe('E-AUTH-03');
|
||||
});
|
||||
|
||||
it('Google 사용자에 붙인다 · 다른 UID 가 쓰는 카카오는 E-AUTH-08', async () => {
|
||||
await getAuth().createUser({ uid: GOOGLE_UID, email: 'fan@fanuzu.co.kr' });
|
||||
expect(await call({ accessToken: 'good', link: true }, GOOGLE_UID)).toEqual({ linked: true });
|
||||
const user = await getAuth().getUser(GOOGLE_UID);
|
||||
expect(user.providerData.some((d) => d.providerId === KAKAO_PROVIDER_ID && d.uid === KAKAO_ID)).toBe(true);
|
||||
|
||||
await getAuth().createUser({ uid: OLD_UID });
|
||||
expect(await codeOf(call({ accessToken: 'good', link: true }, OLD_UID))).toBe('E-AUTH-08');
|
||||
});
|
||||
|
||||
it('이메일이 다르면 붙이지 않는다(different-email) · 카카오 이메일이 없으면 비교를 건너뛴다', async () => {
|
||||
await getAuth().createUser({ uid: GOOGLE_UID, email: 'fan@fanuzu.co.kr' });
|
||||
stubKakao('other@kakao.com');
|
||||
expect(await codeOf(call({ accessToken: 'good', link: true }, GOOGLE_UID))).toBe('different-email');
|
||||
stubKakao('FAN@fanuzu.co.kr');
|
||||
expect(await call({ accessToken: 'good', link: true }, GOOGLE_UID)).toEqual({ linked: true });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user