import { randomUUID } from 'node:crypto'; import { createClient } from 'redis'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { RedisOAuthSessionStore } from '../src/auth/oauthSessionStore.js'; const redisUrl = process.env.GATEWAY_OAUTH_REDIS_TEST_URL; describe.skipIf(!redisUrl)('RedisOAuthSessionStore Kakao login challenge', () => { const prefix = `gateway-oauth-test:${randomUUID()}`; const client = createClient({ url: redisUrl }); const store = new RedisOAuthSessionStore(client, prefix, 300); const userIds = new Set(); const challengeIds = new Set(); beforeAll(async () => { await client.connect(); }); afterAll(async () => { const keys = [ ...[...challengeIds].map((id) => `${prefix}:kakao-login-challenge:${id}`), ...[...userIds].map((id) => `${prefix}:kakao-login-challenge-user:${id}`), ]; if (keys.length > 0) { await client.del(keys); } await client.quit(); }); const createChallenge = async () => { const userId = randomUUID(); userIds.add(userId); const challenge = await store.createLoginChallenge({ userId, code: '4321', attemptsRemaining: 3, expiresAt: new Date(Date.now() + 180_000).toISOString(), createdAt: new Date().toISOString(), }); challengeIds.add(challenge.id); return challenge; }; it('atomically consumes a successful code once', async () => { const challenge = await createChallenge(); const results = await Promise.all([ store.verifyLoginChallenge(challenge.id, challenge.code), store.verifyLoginChallenge(challenge.id, challenge.code), ]); expect(results.filter((result) => result.status === 'verified')).toHaveLength(1); expect(results.filter((result) => result.status === 'expired')).toHaveLength(1); expect(await store.getLoginChallengeForUser(challenge.userId)).toBeNull(); }); it('atomically limits parallel wrong-code submissions to three attempts', async () => { const challenge = await createChallenge(); const results = await Promise.all( Array.from({ length: 4 }, () => store.verifyLoginChallenge(challenge.id, '0000')) ); expect(results.filter((result) => result.status === 'mismatch')).toHaveLength(3); expect(results.filter((result) => result.status === 'locked')).toHaveLength(1); expect( results .filter((result) => result.status === 'mismatch') .map((result) => result.attemptsRemaining) .sort() ).toEqual([0, 1, 2]); await expect(store.verifyLoginChallenge(challenge.id, challenge.code)).resolves.toMatchObject({ status: 'locked', }); }); });