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