Public Access
128 lines
4.0 KiB
TypeScript
128 lines
4.0 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import type { Request, Response, NextFunction } from 'express'
|
|
import * as userRepo from '../repositories/userRepo.js'
|
|
import { signToken } from '../utils/tokens.js'
|
|
import { requireAuth } from './auth.js'
|
|
|
|
function mockRes(): Response {
|
|
const res = {} as Response
|
|
res.status = vi.fn(() => res) as unknown as Response['status']
|
|
res.json = vi.fn(() => res) as unknown as Response['json']
|
|
return res
|
|
}
|
|
|
|
function makeReq(token?: string): Request {
|
|
const headers: Record<string, string> = {}
|
|
if (token) headers.authorization = `Bearer ${token}`
|
|
return { headers } as unknown as Request
|
|
}
|
|
|
|
function createUser(role: 'user' | 'admin' = 'user'): { id: number; email: string; name: string } {
|
|
const email = `user-${Math.random().toString(36).slice(2)}@example.com`
|
|
const { id } = userRepo.createUser({
|
|
email,
|
|
passwordHash: 'x',
|
|
displayName: 'Test User',
|
|
role,
|
|
emailVerified: 1,
|
|
})
|
|
return { id, email, name: 'Test User' }
|
|
}
|
|
|
|
describe('requireAuth', () => {
|
|
it('accepts a valid token and loads fresh user data from the DB', () => {
|
|
const u = createUser()
|
|
const token = signToken({ userId: u.id, email: u.email, displayName: u.name, role: 'user', tv: 0 })
|
|
const req = makeReq(token)
|
|
const res = mockRes()
|
|
const next = vi.fn() as NextFunction
|
|
|
|
requireAuth(req, res, next)
|
|
|
|
expect(next).toHaveBeenCalledOnce()
|
|
expect(res.status).not.toHaveBeenCalled()
|
|
expect(req.user?.userId).toBe(u.id)
|
|
expect(req.user?.role).toBe('user')
|
|
expect(req.user?.tv).toBe(0)
|
|
})
|
|
|
|
it('serves the current DB role, not the role claimed by an old JWT', () => {
|
|
const u = createUser()
|
|
// Token signed while the user was still an admin
|
|
const staleAdminToken = signToken({ userId: u.id, email: u.email, displayName: u.name, role: 'admin', tv: 0 })
|
|
userRepo.updateUser(u.id, { role: 'user' })
|
|
const req = makeReq(staleAdminToken)
|
|
const res = mockRes()
|
|
const next = vi.fn() as NextFunction
|
|
|
|
requireAuth(req, res, next)
|
|
|
|
expect(next).toHaveBeenCalledOnce()
|
|
expect(req.user?.role).toBe('user')
|
|
})
|
|
|
|
it('rejects tokens for deleted accounts', () => {
|
|
const u = createUser()
|
|
const token = signToken({ userId: u.id, email: u.email, displayName: u.name, role: 'user', tv: 0 })
|
|
userRepo.deleteUser(u.id)
|
|
const req = makeReq(token)
|
|
const res = mockRes()
|
|
const next = vi.fn() as NextFunction
|
|
|
|
requireAuth(req, res, next)
|
|
|
|
expect(next).not.toHaveBeenCalled()
|
|
expect(res.status).toHaveBeenCalledWith(401)
|
|
})
|
|
|
|
it('rejects tokens whose token version is stale (e.g. after password change)', () => {
|
|
const u = createUser()
|
|
const oldToken = signToken({ userId: u.id, email: u.email, displayName: u.name, role: 'user', tv: 0 })
|
|
userRepo.updateUser(u.id, { tokenVersion: 1 })
|
|
const req = makeReq(oldToken)
|
|
const res = mockRes()
|
|
const next = vi.fn() as NextFunction
|
|
|
|
requireAuth(req, res, next)
|
|
|
|
expect(next).not.toHaveBeenCalled()
|
|
expect(res.status).toHaveBeenCalledWith(401)
|
|
})
|
|
|
|
it('accepts a token signed with the current token version after rotation', () => {
|
|
const u = createUser()
|
|
userRepo.updateUser(u.id, { tokenVersion: 2 })
|
|
const freshToken = signToken({ userId: u.id, email: u.email, displayName: u.name, role: 'user', tv: 2 })
|
|
const req = makeReq(freshToken)
|
|
const res = mockRes()
|
|
const next = vi.fn() as NextFunction
|
|
|
|
requireAuth(req, res, next)
|
|
|
|
expect(next).toHaveBeenCalledOnce()
|
|
expect(req.user?.tv).toBe(2)
|
|
})
|
|
|
|
it('rejects requests without a Bearer header', () => {
|
|
const req = makeReq()
|
|
const res = mockRes()
|
|
const next = vi.fn() as NextFunction
|
|
|
|
requireAuth(req, res, next)
|
|
|
|
expect(next).not.toHaveBeenCalled()
|
|
expect(res.status).toHaveBeenCalledWith(401)
|
|
})
|
|
|
|
it('rejects garbage tokens', () => {
|
|
const req = makeReq('not.a.jwt')
|
|
const res = mockRes()
|
|
const next = vi.fn() as NextFunction
|
|
|
|
requireAuth(req, res, next)
|
|
|
|
expect(next).not.toHaveBeenCalled()
|
|
expect(res.status).toHaveBeenCalledWith(401)
|
|
})
|
|
})
|