admin/features/auth/useAuthFlow.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

133 lines
4.5 KiB
TypeScript

import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useAuthFlow } from '@/features/auth/useAuthFlow'
import { texts } from '@/texts'
const mocks = vi.hoisted(() => ({
addToast: vi.fn(),
checkLoginOtp: vi.fn(),
completeProfile: vi.fn(),
replace: vi.fn(),
sendLoginOtp: vi.fn(),
}))
vi.mock('@/lib/toast', () => ({ addToast: mocks.addToast }))
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: mocks.replace }),
useSearchParams: () => new URLSearchParams(),
}))
vi.mock('@/hooks/useAuth', () => ({
default: () => ({
checkLoginOtp: mocks.checkLoginOtp,
completeProfile: mocks.completeProfile,
sendLoginOtp: mocks.sendLoginOtp,
user: null,
}),
}))
describe('useAuthFlow registration consent', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.checkLoginOtp.mockResolvedValue('pending')
})
it('completes the OTP login funnel', async () => {
mocks.sendLoginOtp.mockResolvedValue({ purpose: 'login', expiresIn: 120, resendIn: 60 })
mocks.checkLoginOtp.mockResolvedValue('active')
const { result } = renderHook(() => useAuthFlow({ navigateOnSuccess: false }))
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
await act(() => result.current.checkOtp({ code: '1234' }))
expect(result.current.otpTtl).toBe(120)
expect(result.current.resendCodeTime).toBe(120)
})
it('requires accepting terms when the OTP purpose is register', async () => {
mocks.sendLoginOtp.mockResolvedValue({ purpose: 'register', expiresIn: 120, resendIn: 60 })
const { result } = renderHook(() => useAuthFlow({ navigateOnSuccess: false }))
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
expect(result.current.isRegistration).toBe(true)
expect(result.current.termsAccepted).toBe(false)
await act(() => result.current.checkOtp({ code: '1234' }))
expect(mocks.checkLoginOtp).not.toHaveBeenCalled()
expect(result.current.termsError).toBe(texts.auth.termsRequiredMessage)
act(() => {
result.current.updateTermsAccepted(true)
})
await act(() => result.current.checkOtp({ code: '1234' }))
expect(mocks.checkLoginOtp).toHaveBeenCalledWith({
mobile: '09123456789',
code: '1234',
termsAccepted: true,
})
})
it('skips explicit terms acceptance when requireTermsAcceptance is false', async () => {
mocks.sendLoginOtp.mockResolvedValue({ purpose: 'register', expiresIn: 120, resendIn: 60 })
const { result } = renderHook(() => useAuthFlow({ navigateOnSuccess: false, requireTermsAcceptance: false }))
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
await act(() => result.current.checkOtp({ code: '1234' }))
expect(mocks.checkLoginOtp).toHaveBeenCalledWith({
mobile: '09123456789',
code: '1234',
termsAccepted: true,
})
})
it('does not require terms consent for an existing user login', async () => {
mocks.sendLoginOtp.mockResolvedValue({ purpose: 'login', expiresIn: 120, resendIn: 60, alreadySent: false })
const { result } = renderHook(() => useAuthFlow({ navigateOnSuccess: false }))
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
await act(() => result.current.checkOtp({ code: '1234' }))
expect(result.current.isRegistration).toBe(false)
expect(result.current.termsError).toBeNull()
expect(mocks.checkLoginOtp).toHaveBeenCalledOnce()
})
it('reuses a still-valid OTP window instead of restarting the 120-second countdown', async () => {
mocks.sendLoginOtp.mockResolvedValue({ purpose: 'login', expiresIn: 47, resendIn: 60, alreadySent: true })
const { result } = renderHook(() => useAuthFlow({ navigateOnSuccess: false }))
await act(() => result.current.sendOtp({ mobile: '09123456789' }))
expect(result.current.step).toBe('otp')
expect(result.current.otpReused).toBe(true)
expect(result.current.otpTtl).toBe(47)
expect(result.current.resendCodeTime).toBe(47)
})
it('submits the selected city with the completed profile', async () => {
const { result } = renderHook(() => useAuthFlow({ navigateOnSuccess: false }))
await act(() =>
result.current.submitProfile({
firstName: ' علی ',
lastName: ' رضایی ',
gender: 'male',
dateOfBirth: '1991-03-21',
cityId: '12',
})
)
expect(mocks.completeProfile).toHaveBeenCalledWith({
firstName: 'علی',
lastName: 'رضایی',
gender: 'male',
dateOfBirth: '1991-03-21',
cityId: 12,
})
})
})