Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
125 lines
4.2 KiB
TypeScript
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)
|
|
})
|
|
})
|