OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-web-ui / test / router.test.js

231 lines · javascript ref:
import test from 'brittle'
import { Router, json, html, readJson, parsePagination } from '../lib/router.js'

// Mock req/res
function mockReq (method, url, opts = {}) {
  const req = {
    method,
    url,
    headers: opts.headers || {},
    params: {},
    query: {},
    [Symbol.asyncIterator]: async function * () {
      if (opts.body) yield Buffer.from(JSON.stringify(opts.body))
    }
  }
  return req
}

function mockRes () {
  const res = {
    statusCode: null,
    headers: {},
    _setHeaders: {},
    body: '',
    writableEnded: false,
    writeHead (status, headers) { res.statusCode = status; res.headers = { ...res._setHeaders, ...headers } },
    setHeader (k, v) { res._setHeaders[k] = v },
    end (data) { res.body = data || ''; res.writableEnded = true },
    write (data) { res.body += data }
  }
  return res
}

test('router matches pattern with params', async (t) => {
  const router = new Router()
  let captured = null

  router.get('/api/repos/:name', async (req) => {
    captured = req.params
    return { name: req.params.name }
  })

  const req = mockReq('GET', '/api/repos/pear-git')
  const res = mockRes()
  const handled = await router.handle(req, res)

  t.is(handled, true)
  t.is(captured.name, 'pear-git')
  t.is(res.statusCode, 200)
  t.ok(res.body.includes('pear-git'))
})

test('router returns JSON by default', async (t) => {
  const router = new Router()
  router.get('/api/status', async () => ({ ok: true }))

  const req = mockReq('GET', '/api/status')
  const res = mockRes()
  await router.handle(req, res)

  t.is(res.headers['Content-Type'], 'application/json')
  t.is(JSON.parse(res.body).ok, true)
})

test('content negotiation — SSR when Accept: text/html + renderer', async (t) => {
  const router = new Router()
  router.get('/repos/:name', async (req) => ({ name: req.params.name }), {
    render: (data) => `<h1>${data.name}</h1>`
  })

  const req = mockReq('GET', '/repos/pear-git', { headers: { accept: 'text/html' } })
  const res = mockRes()
  await router.handle(req, res)

  t.is(res.headers['Content-Type'], 'text/html; charset=utf-8')
  t.ok(res.body.includes('<h1>pear-git</h1>'))
})

test('content negotiation — JSON when Accept: text/html but NO renderer', async (t) => {
  const router = new Router()
  router.get('/api/repos/:name', async (req) => ({ name: req.params.name }))

  const req = mockReq('GET', '/api/repos/pear-git', { headers: { accept: 'text/html' } })
  const res = mockRes()
  await router.handle(req, res)

  t.is(res.headers['Content-Type'], 'application/json')
})

test('router handles prefix', async (t) => {
  const router = new Router('/git')
  router.get('/api/status', async () => ({ ok: true }))

  const req = mockReq('GET', '/git/api/status')
  const res = mockRes()
  const handled = await router.handle(req, res)

  t.is(handled, true)
  t.is(JSON.parse(res.body).ok, true)
})

test('router returns false for unmatched routes', async (t) => {
  const router = new Router()
  router.get('/api/status', async () => ({ ok: true }))

  const req = mockReq('GET', '/api/nonexistent')
  const res = mockRes()
  const handled = await router.handle(req, res)

  t.is(handled, false)
})

test('query string parsing', async (t) => {
  const router = new Router()
  let captured = null
  router.get('/api/repos', async (req) => { captured = req.query; return {} })

  const req = mockReq('GET', '/api/repos?status=open&sort=newest')
  const res = mockRes()
  await router.handle(req, res)

  t.is(captured.status, 'open')
  t.is(captured.sort, 'newest')
})

test('POST with JSON body', async (t) => {
  const router = new Router()
  let captured = null

  router.post('/api/issues', async (req) => {
    captured = await readJson(req)
    return { id: 1 }
  })

  const req = mockReq('POST', '/api/issues', { body: { title: 'Bug', author: 'alice' } })
  const res = mockRes()
  await router.handle(req, res)

  t.is(captured.title, 'Bug')
  t.is(captured.author, 'alice')
  t.is(JSON.parse(res.body).id, 1)
  t.is(res.statusCode, 200)
})

test('error handler returns 500', async (t) => {
  const router = new Router()
  router.get('/api/crash', async () => { throw new Error('boom') })

  const req = mockReq('GET', '/api/crash')
  const res = mockRes()
  await router.handle(req, res)

  t.is(res.statusCode, 500)
  t.is(JSON.parse(res.body).error, 'boom')
})

// ── parsePagination ───────────────────────────────────────

test('parsePagination — defaults', (t) => {
  const p = parsePagination({})
  t.is(p.limit, 20)
  t.is(p.offset, 0)
})

test('parsePagination — custom values', (t) => {
  const p = parsePagination({ limit: '50', offset: '10' })
  t.is(p.limit, 50)
  t.is(p.offset, 10)
})

test('parsePagination — clamps to maxLimit', (t) => {
  const p = parsePagination({ limit: '999' }, { maxLimit: 50 })
  t.is(p.limit, 50)
})

test('parsePagination — rejects negative/NaN', (t) => {
  const p = parsePagination({ limit: '-5', offset: 'abc' })
  t.is(p.limit, 20)
  t.is(p.offset, 0)
})

// ── CORS ──────────────────────────────────────────────────

test('CORS preflight returns 204', async (t) => {
  const router = new Router('', { cors: true })
  router.get('/api/status', async () => ({ ok: true }))

  const req = mockReq('OPTIONS', '/api/status')
  const res = mockRes()
  const handled = await router.handle(req, res)

  t.is(handled, true)
  t.is(res.statusCode, 204)
  t.is(res.headers['Access-Control-Allow-Origin'], '*')
  t.is(res.headers['Access-Control-Allow-Methods'], 'GET, POST, PATCH, DELETE, OPTIONS')
})

test('CORS headers on JSON response', async (t) => {
  const router = new Router('', { cors: true })
  router.get('/api/data', async () => ({ x: 1 }))

  const req = mockReq('GET', '/api/data')
  const res = mockRes()
  await router.handle(req, res)

  t.is(res.headers['Access-Control-Allow-Origin'], '*')
  t.is(res.statusCode, 200)
})

test('CORS custom origin', async (t) => {
  const router = new Router('', { cors: true, corsOrigin: 'https://example.com' })
  router.get('/api/data', async () => ({ x: 1 }))

  const req = mockReq('GET', '/api/data')
  const res = mockRes()
  await router.handle(req, res)

  t.is(res.headers['Access-Control-Allow-Origin'], 'https://example.com')
})

test('CORS disabled by default on existing routers (backward compat)', async (t) => {
  const router = new Router()
  router.get('/api/data', async () => ({ x: 1 }))

  const req = mockReq('GET', '/api/data')
  const res = mockRes()
  await router.handle(req, res)

  t.is(res.headers['Access-Control-Allow-Origin'], undefined)
})