feat(ST-1135): UA-redirect bij login — phone naar /m/* (T-322/T-323/T-324)
- lib/user-agent.ts (nieuw): isPhoneUA() — Mobi-substring heuristiek (telefoons hebben Mobi, tablets/desktop niet) - actions/auth.ts loginAction: leest user-agent header na session.save(); phone-UA + actief product → /m/products/[id]/solo, zonder → /m/settings; tablet/desktop/null-UA → /dashboard (ongewijzigd) - Tests: 7 helper-cases + 6 loginAction-paden incl. demo-user Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
479a502dfd
commit
13ab53ab8d
4 changed files with 138 additions and 0 deletions
85
__tests__/actions/auth.test.ts
Normal file
85
__tests__/actions/auth.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const { redirectMock, verifyUserMock, headerGetMock, sessionSaveMock } = vi.hoisted(() => ({
|
||||
redirectMock: vi.fn((path: string) => { throw new Error(`REDIRECT:${path}`) }),
|
||||
verifyUserMock: vi.fn(),
|
||||
headerGetMock: vi.fn(),
|
||||
sessionSaveMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({ redirect: redirectMock }))
|
||||
vi.mock('next/headers', () => ({
|
||||
cookies: vi.fn().mockResolvedValue({}),
|
||||
headers: vi.fn().mockResolvedValue({ get: headerGetMock }),
|
||||
}))
|
||||
vi.mock('iron-session', () => ({
|
||||
getIronSession: vi.fn().mockResolvedValue({
|
||||
userId: '',
|
||||
isDemo: false,
|
||||
save: sessionSaveMock,
|
||||
}),
|
||||
}))
|
||||
vi.mock('@/lib/session', () => ({ sessionOptions: { cookieName: 't', password: 't' } }))
|
||||
vi.mock('@/lib/auth', () => ({
|
||||
verifyUser: verifyUserMock,
|
||||
registerUser: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/lib/rate-limit', () => ({ checkRateLimit: vi.fn().mockReturnValue(true) }))
|
||||
|
||||
import { loginAction } from '@/actions/auth'
|
||||
|
||||
const IPHONE_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) Mobile/15E148 Safari/604.1'
|
||||
const IPAD_UA = 'Mozilla/5.0 (iPad; CPU OS 17_4 like Mac OS X) Safari/604.1'
|
||||
const DESKTOP_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) Chrome/124.0.0.0 Safari/537.36'
|
||||
|
||||
function fd(username: string, password: string) {
|
||||
const f = new FormData()
|
||||
f.set('username', username)
|
||||
f.set('password', password)
|
||||
return f
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
redirectMock.mockClear()
|
||||
verifyUserMock.mockReset()
|
||||
headerGetMock.mockReset()
|
||||
sessionSaveMock.mockReset()
|
||||
})
|
||||
|
||||
describe('loginAction UA-redirect', () => {
|
||||
it('phone-UA + actief product → /m/products/[id]/solo', async () => {
|
||||
verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: 'p1' })
|
||||
headerGetMock.mockReturnValue(IPHONE_UA)
|
||||
await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/m/products/p1/solo')
|
||||
})
|
||||
|
||||
it('phone-UA zonder actief product → /m/settings', async () => {
|
||||
verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: null })
|
||||
headerGetMock.mockReturnValue(IPHONE_UA)
|
||||
await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/m/settings')
|
||||
})
|
||||
|
||||
it('tablet-UA (iPad) → /dashboard', async () => {
|
||||
verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: 'p1' })
|
||||
headerGetMock.mockReturnValue(IPAD_UA)
|
||||
await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/dashboard')
|
||||
})
|
||||
|
||||
it('desktop-UA → /dashboard', async () => {
|
||||
verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: 'p1' })
|
||||
headerGetMock.mockReturnValue(DESKTOP_UA)
|
||||
await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/dashboard')
|
||||
})
|
||||
|
||||
it('geen UA-header → /dashboard', async () => {
|
||||
verifyUserMock.mockResolvedValue({ id: 'u1', is_demo: false, active_product_id: 'p1' })
|
||||
headerGetMock.mockReturnValue(null)
|
||||
await expect(loginAction(undefined, fd('alice', 'secret123'))).rejects.toThrow('REDIRECT:/dashboard')
|
||||
})
|
||||
|
||||
it('demo-user op phone volgt dezelfde routing', async () => {
|
||||
verifyUserMock.mockResolvedValue({ id: 'demo', is_demo: true, active_product_id: 'p1' })
|
||||
headerGetMock.mockReturnValue(IPHONE_UA)
|
||||
await expect(loginAction(undefined, fd('demo', 'demo123pw'))).rejects.toThrow('REDIRECT:/m/products/p1/solo')
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue