admin/features/auth/useAuthFlow.functional.test.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

125 lines
4.2 KiB
TypeScript

import { act, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
checkLoginOtp: vi.fn(),
replace: vi.fn(),
sendLoginOtp: vi.fn(),
}))
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: mocks.replace }),
useSearchParams: () => new URLSearchParams('redirect=%2Fbookings%2Fbooking-1'),
}))
vi.mock('@/hooks/useAuth', () => ({
default: () => ({
checkLoginOtp: mocks.checkLoginOtp,
completeProfile: vi.fn(),
sendLoginOtp: mocks.sendLoginOtp,
user: null,
}),
}))
vi.mock('@/lib/toast', () => ({ addToast: vi.fn() }))
vi.mock('@/lib/analytics', () => ({
ANALYTICS_EVENTS: {
AUTH_FAILED: 'auth_failed',
AUTH_OPENED: 'auth_opened',
LOGIN: 'login',
OTP_REQUESTED: 'otp_requested',
},
trackAnalyticsEvent: vi.fn(),
}))
vi.mock('@/hooks/useSmsOtpAutofill', () => ({ useSmsOtpAutofill: () => undefined }))
import { useAuthFlow } from '@/features/auth/useAuthFlow'
describe('useAuthFlow functional OTP journey', () => {
beforeEach(() => {
mocks.sendLoginOtp.mockResolvedValue({ purpose: 'login', expiresIn: 120, resendIn: 60 })
mocks.checkLoginOtp.mockResolvedValue('active')
})
afterEach(() => {
vi.clearAllMocks()
})
it('normalizes the entered mobile, verifies the OTP, then redirects to the requested consumer destination', async () => {
const { result } = renderHook(() => useAuthFlow())
await act(() => result.current.sendOtp({ mobile: '۰۹۱۲۳۴۵۶۷۸۹' }))
expect(mocks.sendLoginOtp).toHaveBeenCalledWith({ mobile: '09123456789' })
expect(result.current.step).toBe('otp')
await act(() => result.current.checkOtp({ code: '۱۲۳۴' }))
expect(mocks.checkLoginOtp).toHaveBeenCalledWith({ mobile: '09123456789', code: '1234' })
expect(mocks.replace).toHaveBeenCalledWith('/bookings/booking-1')
})
it('keeps the user on the OTP step and does not redirect when verification fails', async () => {
const invalidOtp = Object.assign(new Error('invalid otp'), { apiCode: 'OTP_INVALID' })
mocks.checkLoginOtp.mockRejectedValue(invalidOtp)
const { result } = renderHook(() => useAuthFlow())
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
await act(() => result.current.checkOtp({ code: '1234' }))
expect(result.current.step).toBe('otp')
expect(result.current.otpError).not.toBeNull()
expect(mocks.replace).not.toHaveBeenCalled()
})
it('moves a pending user to profile completion instead of navigating away', async () => {
mocks.checkLoginOtp.mockResolvedValue('pending')
const { result } = renderHook(() => useAuthFlow())
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
await act(() => result.current.checkOtp({ code: '1234' }))
expect(result.current.step).toBe('profile')
expect(mocks.replace).not.toHaveBeenCalled()
})
it('auto-submits when the OTP field reaches the required length', async () => {
const { result } = renderHook(() => useAuthFlow())
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
act(() => {
result.current.checkOtpForm.setValue('code', '1234', { shouldDirty: true, shouldValidate: true })
})
await act(async () => {
await Promise.resolve()
})
expect(mocks.checkLoginOtp).toHaveBeenCalledWith({ mobile: '09123456789', code: '1234' })
expect(mocks.replace).toHaveBeenCalledWith('/bookings/booking-1')
})
it('does not re-submit the same completed OTP after a failed verification', async () => {
const invalidOtp = Object.assign(new Error('invalid otp'), { apiCode: 'OTP_INVALID' })
mocks.checkLoginOtp.mockRejectedValue(invalidOtp)
const { result } = renderHook(() => useAuthFlow())
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
act(() => {
result.current.checkOtpForm.setValue('code', '1234', { shouldDirty: true, shouldValidate: true })
})
await act(async () => {
await Promise.resolve()
})
expect(mocks.checkLoginOtp).toHaveBeenCalledTimes(1)
await act(async () => {
await Promise.resolve()
})
expect(mocks.checkLoginOtp).toHaveBeenCalledTimes(1)
})
})