import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { serveStatic, toSSG } from '../../src/adapter/bun' import { createBunWebSocket } from '../../src/adapter/bun/websocket' import type { BunWebSocketData } from '../../src/adapter/bun/websocket' import { Context } from '../../src/context' import { env, getRuntimeKey } from '../../src/helper/adapter' import type { WSMessageReceive } from '../../src/helper/websocket' import { Hono } from '../../src/index' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { jsx } from '../../src/jsx' import { basicAuth } from '../../src/middleware/basic-auth' import { jwt } from '../../src/middleware/jwt' import { HonoRequest } from '../../src/request' import { stream, streamSSE } from '../..//src/helper/streaming' // Test just only minimal patterns. // Because others are tested well in Cloudflare Workers environment already. Bun.env.NAME = 'Bun' describe('Basic', () => { const app = new Hono() app.get('/a/:foo', (c) => { c.header('x-param', c.req.param('foo')) c.header('x-query', c.req.query('q')) return c.text('Hello Bun!') }) it('Should return 200 Response', async () => { const req = new Request('http://localhost/a/foo?q=bar') const res = await app.request(req) expect(res.status).toBe(200) expect(await res.text()).toBe('Hello Bun!') expect(res.headers.get('x-param')).toBe('foo') expect(res.headers.get('x-query')).toBe('bar') }) it('returns current runtime (bun)', async () => { expect(getRuntimeKey()).toBe('bun') }) }) describe('Environment Variables', () => { it('Should return the environment variable', async () => { const c = new Context(new HonoRequest(new Request('http://localhost/'))) const { NAME } = env<{ NAME: string }>(c) expect(NAME).toBe('Bun') }) }) describe('Basic Auth Middleware', () => { const app = new Hono() const username = 'hono-user-a' const password = 'hono-password-a' app.use( '/auth/*', basicAuth({ username, password, }) ) app.get('/auth/*', () => new Response('auth')) it('Should not authorize, return 401 Response', async () => { const req = new Request('http://localhost/auth/a') const res = await app.request(req) expect(res.status).toBe(401) expect(await res.text()).toBe('Unauthorized') }) it('Should authorize, return 200 Response', async () => { const credential = 'aG9uby11c2VyLWE6aG9uby1wYXNzd29yZC1h' const req = new Request('http://localhost/auth/a') req.headers.set('Authorization', `Basic ${credential}`) const res = await app.request(req) expect(res.status).toBe(200) expect(await res.text()).toBe('auth') }) }) describe('Serve Static Middleware', () => { const app = new Hono() const onNotFound = vi.fn(() => {}) app.all('/favicon.ico', serveStatic({ path: './runtime_tests/bun/favicon.ico' })) app.all( '/favicon-notfound.ico', serveStatic({ path: './runtime_tests/bun/favicon-notfound.ico', onNotFound }) ) app.use('/favicon-notfound.ico', async (c, next) => { await next() c.header('X-Custom', 'Bun') }) app.get( '/static/*', serveStatic({ root: './runtime_tests/bun/', onNotFound, }) ) app.get( '/dot-static/*', serveStatic({ root: './runtime_tests/bun/', rewriteRequestPath: (path) => path.replace(/^\/dot-static/, './.static'), }) ) beforeEach(() => onNotFound.mockClear()) it('Should return static file correctly', async () => { const res = await app.request(new Request('http://localhost/favicon.ico')) await res.arrayBuffer() expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('image/x-icon') }) it('Should return 404 response', async () => { const res = await app.request(new Request('http://localhost/favicon-notfound.ico')) expect(res.status).toBe(404) expect(res.headers.get('X-Custom')).toBe('Bun') expect(onNotFound).toHaveBeenCalledWith( './runtime_tests/bun/favicon-notfound.ico', expect.anything() ) }) it('Should return 200 response - /static/plain.txt', async () => { const res = await app.request(new Request('http://localhost/static/plain.txt')) expect(res.status).toBe(200) expect(await res.text()).toBe('Bun!') expect(onNotFound).not.toHaveBeenCalled() }) it('Should return 200 response - /static/download', async () => { const res = await app.request(new Request('http://localhost/static/download')) expect(res.status).toBe(200) expect(await res.text()).toBe('download') expect(onNotFound).not.toHaveBeenCalled() }) it('Should return 200 response - /dot-static/plain.txt', async () => { const res = await app.request(new Request('http://localhost/dot-static/plain.txt')) expect(res.status).toBe(200) expect(await res.text()).toBe('Bun!!') }) it('Should return 200 response - /static/helloworld', async () => { const res = await app.request('http://localhost/static/helloworld') expect(res.status).toBe(200) expect(await res.text()).toBe('Hi\n') }) it('Should return 200 response - /static/hello.world', async () => { const res = await app.request('http://localhost/static/hello.world') expect(res.status).toBe(200) expect(await res.text()).toBe('Hi\n') }) }) // Bun support WebCrypto since v0.2.2 // So, JWT middleware works well. describe('JWT Auth Middleware', () => { const app = new Hono() app.use('/jwt/*', jwt({ secret: 'a-secret' })) app.get('/jwt/a', (c) => c.text('auth')) it('Should not authorize, return 401 Response', async () => { const req = new Request('http://localhost/jwt/a') const res = await app.request(req) expect(res.status).toBe(401) expect(await res.text()).toBe('Unauthorized') }) it('Should authorize, return 200 Response', async () => { const credential = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.B54pAqIiLbu170tGQ1rY06Twv__0qSHTA0ioQPIOvFE' const req = new Request('http://localhost/jwt/a') req.headers.set('Authorization', `Bearer ${credential}`) const res = await app.request(req) expect(res.status).toBe(200) expect(await res.text()).toBe('auth') }) }) // To enable JSX middleware, // set "jsxImportSource": "hono/jsx" in the tsconfig.json describe('JSX Middleware', () => { const app = new Hono() const Layout = (props: { children?: string }) => { return {props.children} } app.get('/', (c) => { return c.html(

Hello

) }) app.get('/nest', (c) => { return c.html(

Hello

) }) app.get('/layout', (c) => { return c.html(

hello

) }) it('Should return rendered HTML', async () => { const res = await app.request(new Request('http://localhost/')) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8') expect(await res.text()).toBe('

Hello

') }) it('Should return rendered HTML with nest', async () => { const res = await app.request(new Request('http://localhost/nest')) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8') expect(await res.text()).toBe('

Hello

') }) it('Should return rendered HTML with Layout', async () => { const res = await app.request(new Request('http://localhost/layout')) expect(res.status).toBe(200) expect(res.headers.get('Content-Type')).toBe('text/html; charset=UTF-8') expect(await res.text()).toBe('

hello

') }) }) describe('toSSG function', () => { let app: Hono beforeEach(() => { app = new Hono() app.get('/', (c) => c.text('Hello, World!')) app.get('/about', (c) => c.text('About Page')) app.get('/about/some', (c) => c.text('About Page 2tier')) app.post('/about/some/thing', (c) => c.text('About Page 3tier')) app.get('/bravo', (c) => c.html('Bravo Page')) app.get('/Charlie', async (c, next) => { c.setRenderer((content, head) => { return c.html( {head.title || ''}

{content}

) }) await next() }) app.get('/Charlie', (c) => { return c.render('Hello!', { title: 'Charlies Page' }) }) }) it('Should correctly generate static HTML files for Hono routes', async () => { const result = await toSSG(app, { dir: './static' }) expect(result.success).toBeTruly expect(result.error).toBeUndefined() expect(result.files).toBeDefined() afterAll(async () => { await deleteDirectory('./static') }) }) }) describe('WebSockets Helper', () => { const app = new Hono() const { websocket, upgradeWebSocket } = createBunWebSocket() it('Should websockets is working', async () => { const receivedMessagePromise = new Promise((resolve) => app.get( '/ws', upgradeWebSocket(() => ({ onMessage(evt) { resolve(evt.data) }, })) ) ) const upgradedData = await new Promise((resolve) => app.fetch(new Request('http://localhost/ws'), { upgrade: (_req: Request, data: { data: BunWebSocketData }) => { resolve(data.data) }, }) ) const message = Math.random().toString() websocket.message( { close: () => undefined, readyState: 3, data: upgradedData, send: () => undefined, }, message ) const receivedMessage = await receivedMessagePromise expect(receivedMessage).toBe(message) }) }) const fs = require('fs').promises const path = require('path') async function deleteDirectory(dirPath) { if ( await fs .stat(dirPath) .then((stat) => stat.isDirectory()) .catch(() => false) ) { for (const entry of await fs.readdir(dirPath)) { const entryPath = path.join(dirPath, entry) await deleteDirectory(entryPath) } await fs.rmdir(dirPath) } else { await fs.unlink(dirPath) } } describe('streaming', () => { const app = new Hono() let server: ReturnType let aborted = false app.get('/stream', (c) => { return stream(c, async (stream) => { stream.onAbort(() => { aborted = true }) return new Promise((resolve) => { stream.onAbort(resolve) }) }) }) app.get('/streamHello', (c) => { return stream(c, async (stream) => { stream.onAbort(() => { aborted = true }) await stream.write('Hello') }) }) app.get('/streamSSE', (c) => { return streamSSE(c, async (stream) => { stream.onAbort(() => { aborted = true }) return new Promise((resolve) => { stream.onAbort(resolve) }) }) }) app.get('/streamSSEHello', (c) => { return streamSSE(c, async (stream) => { stream.onAbort(() => { aborted = true }) await stream.write('Hello') }) }) beforeEach(() => { aborted = false server = Bun.serve({ port: 0, fetch: app.fetch }) }) afterEach(() => { server.stop() }) describe('stream', () => { it('Should call onAbort', async () => { const ac = new AbortController() const req = new Request(`http://localhost:${server.port}/stream`, { signal: ac.signal, }) expect(aborted).toBe(false) const res = fetch(req).catch(() => {}) await new Promise((resolve) => setTimeout(resolve, 10)) ac.abort() await res while (!aborted) { await new Promise((resolve) => setTimeout(resolve)) } expect(aborted).toBe(true) }) it('Should not be called onAbort if already closed', async () => { expect(aborted).toBe(false) const res = await fetch(`http://localhost:${server.port}/streamHello`) expect(await res.text()).toBe('Hello') expect(aborted).toBe(false) }) }) describe('streamSSE', () => { it('Should call onAbort', async () => { const ac = new AbortController() const req = new Request(`http://localhost:${server.port}/streamSSE`, { signal: ac.signal, }) const res = fetch(req).catch(() => {}) await new Promise((resolve) => setTimeout(resolve, 10)) ac.abort() await res while (!aborted) { await new Promise((resolve) => setTimeout(resolve)) } expect(aborted).toBe(true) }) it('Should not be called onAbort if already closed', async () => { expect(aborted).toBe(false) const res = await fetch(`http://localhost:${server.port}/streamSSEHello`) expect(await res.text()).toBe('Hello') expect(aborted).toBe(false) }) }) })