Strengthen nickname uniqueness validation with Firestore fallback.
- RTDB 예약 시스템에만 의존하던 닉네임 중복 검증에 Firestore 조회를 통한 fallback 로직을 추가했습니다. - `consumeReservation`을 검증(`verify`)과 삭제(`delete`) 단계로 분리하여 유저 생성 과정의 안정성을 높였습니다. - 이미 가입이 완료되어 RTDB 예약 기록이 삭제된 닉네임에 대해서도 중복 가입을 차단하도록 개선했습니다. - 확정된 유저의 닉네임 중복 요청 시 409 NICKNAME_TAKEN 에러를 반환하는 테스트 케이스를 추가했습니다.
This commit is contained in:
parent
dfd21ba4c2
commit
697e0ff11a
@ -56,7 +56,7 @@ export async function reserveNickname(
|
||||
return { previousReservation };
|
||||
}
|
||||
|
||||
export async function consumeReservation(
|
||||
export async function verifyReservation(
|
||||
uid: string,
|
||||
name: string
|
||||
): Promise<void> {
|
||||
@ -77,7 +77,12 @@ export async function consumeReservation(
|
||||
"RESERVATION_MISSING"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteReservation(
|
||||
uid: string,
|
||||
name: string
|
||||
): Promise<void> {
|
||||
await rtdb.ref().update({
|
||||
[userNicknamePath(uid)]: null,
|
||||
[nicknamePath(name)]: null,
|
||||
|
||||
@ -29,6 +29,21 @@ export async function getUser(uid: string): Promise<User | null> {
|
||||
return snap.exists ? (snap.data() as User) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* displayName으로 유저의 uid를 조회한다. 없으면 `null`.
|
||||
* 닉네임 유니크성 검증용 fallback.
|
||||
*/
|
||||
export async function findUidByDisplayName(
|
||||
displayName: string
|
||||
): Promise<string | null> {
|
||||
const snap = await firestore
|
||||
.collection(COLLECTION)
|
||||
.where("displayName", "==", displayName)
|
||||
.limit(1)
|
||||
.get();
|
||||
return snap.empty ? null : snap.docs[0].id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 신규 유저 문서를 생성한다. `createdAt`은 서버 타임스탬프로 기록되며,
|
||||
* 같은 UID가 있으면 전체 덮어쓴다(`merge: false`).
|
||||
|
||||
@ -4,13 +4,15 @@ import { auth } from "../firebase.js";
|
||||
import {
|
||||
createUser,
|
||||
deleteUser,
|
||||
findUidByDisplayName,
|
||||
getUser,
|
||||
updateUser,
|
||||
} from "../repositories/userRepository.js";
|
||||
import {
|
||||
consumeReservation,
|
||||
deleteReservation,
|
||||
releaseReservation,
|
||||
reserveNickname,
|
||||
verifyReservation,
|
||||
} from "../repositories/nicknameRepository.js";
|
||||
import {
|
||||
KnowledgeLevel,
|
||||
@ -119,7 +121,7 @@ export async function createMe(
|
||||
}
|
||||
const provider = providerFromToken(token);
|
||||
|
||||
await consumeReservation(token.uid, displayName);
|
||||
await verifyReservation(token.uid, displayName);
|
||||
|
||||
await createUser(token.uid, {
|
||||
displayName,
|
||||
@ -130,6 +132,8 @@ export async function createMe(
|
||||
knowledgeLevel,
|
||||
});
|
||||
|
||||
await deleteReservation(token.uid, displayName);
|
||||
|
||||
const created = await getUser(token.uid);
|
||||
if (!created) {
|
||||
throw new HttpError(500, "failed to read created user");
|
||||
@ -171,6 +175,11 @@ export async function checkNickname(
|
||||
throw new HttpError(409, "user already exists", "USER_ALREADY_EXISTS");
|
||||
}
|
||||
|
||||
const confirmedOwner = await findUidByDisplayName(displayName);
|
||||
if (confirmedOwner && confirmedOwner !== token.uid) {
|
||||
throw new HttpError(409, "nickname already taken", "NICKNAME_TAKEN");
|
||||
}
|
||||
|
||||
const { previousReservation } = await reserveNickname(
|
||||
token.uid,
|
||||
displayName
|
||||
|
||||
@ -223,6 +223,26 @@ describe("userService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("확정된 유저의 displayName이면 타 uid는 409 NICKNAME_TAKEN", async () => {
|
||||
// userA 가입 후 RTDB 예약은 소비된 상태
|
||||
await createMeWithReservation(fakeToken({ uid: "userA", sub: "userA" }));
|
||||
const nameSnap = await rtdb
|
||||
.ref(`/nicknames/${validBody.displayName}`)
|
||||
.get();
|
||||
expect(nameSnap.exists()).toBe(false); // 선결 조건
|
||||
|
||||
// userB가 같은 이름 요청 → Firestore fallback이 차단
|
||||
await expect(
|
||||
checkNickname(
|
||||
fakeToken({ uid: "userB", sub: "userB" }),
|
||||
validBody.displayName
|
||||
)
|
||||
).rejects.toMatchObject({
|
||||
status: 409,
|
||||
code: "NICKNAME_TAKEN",
|
||||
});
|
||||
});
|
||||
|
||||
it("displayName 형식 오류 → 400", async () => {
|
||||
await expect(checkNickname(fakeToken(), "")).rejects.toMatchObject({
|
||||
status: 400,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user