diff --git a/src/adapter/cloudflare-pages/handler.test.ts b/src/adapter/cloudflare-pages/handler.test.ts index 8f57b7c8..d08e31dc 100644 --- a/src/adapter/cloudflare-pages/handler.test.ts +++ b/src/adapter/cloudflare-pages/handler.test.ts @@ -2,12 +2,31 @@ import { getCookie } from '../../helper/cookie' import { Hono } from '../../hono' import { HTTPException } from '../../http-exception' import type { EventContext } from './handler' -import { handle, handleMiddleware } from './handler' +import { handle, handleMiddleware, serveStatic } from './handler' type Env = { Bindings: { TOKEN: string - eventContext: EventContext + } +} + +function createEventContext( + context: Partial> +): EventContext { + return { + data: {}, + env: { + ...context.env, + ASSETS: { fetch: vi.fn(), ...context.env?.ASSETS }, + TOKEN: context.env?.TOKEN ?? 'HONOISCOOL', + }, + functionPath: '_worker.js', + next: vi.fn(), + params: {}, + passThroughOnException: vi.fn(), + request: new Request('http://localhost/api/foo'), + waitUntil: vi.fn(), + ...context, } } @@ -15,17 +34,29 @@ describe('Adapter for Cloudflare Pages', () => { it('Should return 200 response', async () => { const request = new Request('http://localhost/api/foo') const env = { + ASSETS: { fetch }, TOKEN: 'HONOISCOOL', } + const waitUntil = vi.fn() + const passThroughOnException = vi.fn() + const eventContext = createEventContext({ + request, + env, + waitUntil, + passThroughOnException, + }) const app = new Hono() + const appFetchSpy = vi.spyOn(app, 'fetch') app.get('/api/foo', (c) => { - const reqInEventContext = c.env.eventContext.request - return c.json({ TOKEN: c.env.TOKEN, requestURL: reqInEventContext.url }) + return c.json({ TOKEN: c.env.TOKEN, requestURL: c.req.url }) }) const handler = handle(app) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const res = await handler({ request, env }) + const res = await handler(eventContext) + expect(appFetchSpy).toHaveBeenCalledWith( + request, + { ...env, eventContext }, + { waitUntil, passThroughOnException } + ) expect(res.status).toBe(200) expect(await res.json()).toEqual({ TOKEN: 'HONOISCOOL', @@ -35,6 +66,7 @@ describe('Adapter for Cloudflare Pages', () => { it('Should not use `basePath()` if path argument is not passed', async () => { const request = new Request('http://localhost/api/error') + const eventContext = createEventContext({ request }) const app = new Hono().basePath('/api') app.onError((e) => { @@ -46,9 +78,7 @@ describe('Adapter for Cloudflare Pages', () => { const handler = handle(app) // It does throw the error if app is NOT "subApp" - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(() => handler({ request })).toThrowError('Custom Error') + expect(() => handler(eventContext)).toThrowError('Custom Error') }) }) @@ -59,9 +89,8 @@ describe('Middleware adapter for Cloudflare Pages', () => { Cookie: 'my_cookie=1234', }, }) - const env = { - TOKEN: 'HONOISCOOL', - } + const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + const eventContext = createEventContext({ request, next }) const handler = handleMiddleware(async (c, next) => { const cookie = getCookie(c, 'my_cookie') @@ -70,10 +99,7 @@ describe('Middleware adapter for Cloudflare Pages', () => { return c.json({ cookie, response: await c.res.json() }) }) - const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const res = await handler({ request, env, next }) + const res = await handler(eventContext) expect(next).toHaveBeenCalled() @@ -85,9 +111,6 @@ describe('Middleware adapter for Cloudflare Pages', () => { it('Should return the middleware response when exceptions are handled', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware(async (c, next) => { await next() @@ -95,9 +118,8 @@ describe('Middleware adapter for Cloudflare Pages', () => { }) const next = vi.fn().mockRejectedValue(new Error('Error from next()')) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const res = await handler({ request, env, next }) + const eventContext = createEventContext({ request, next }) + const res = await handler(eventContext) expect(next).toHaveBeenCalled() @@ -108,17 +130,13 @@ describe('Middleware adapter for Cloudflare Pages', () => { it('Should return the middleware response if next() is not called', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware(async (c) => { return c.json({ response: 'Skip Cloudflare Pages' }) }) const next = vi.fn() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const res = await handler({ request, env, next }) + const eventContext = createEventContext({ request, next }) + const res = await handler(eventContext) expect(next).not.toHaveBeenCalled() @@ -129,15 +147,11 @@ describe('Middleware adapter for Cloudflare Pages', () => { it('Should return the Pages response if the middleware does not return a response', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware((c, next) => next()) const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const res = await handler({ request, env, next }) + const eventContext = createEventContext({ request, next }) + const res = await handler(eventContext) expect(next).toHaveBeenCalled() @@ -146,18 +160,14 @@ describe('Middleware adapter for Cloudflare Pages', () => { it('Should handle a HTTPException by returning error.getResponse()', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware(() => { const res = new Response('Unauthorized', { status: 401 }) throw new HTTPException(401, { res }) }) const next = vi.fn() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const res = await handler({ request, env, next }) + const eventContext = createEventContext({ request, next }) + const res = await handler(eventContext) expect(next).not.toHaveBeenCalled() @@ -167,17 +177,13 @@ describe('Middleware adapter for Cloudflare Pages', () => { it('Should handle an HTTPException thrown by next()', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware((c, next) => next()) const next = vi .fn() .mockRejectedValue(new HTTPException(401, { res: Response.json('Unauthorized') })) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const res = await handler({ request, env, next }) + const eventContext = createEventContext({ request, next }) + const res = await handler(eventContext) expect(next).toHaveBeenCalled() @@ -186,58 +192,81 @@ describe('Middleware adapter for Cloudflare Pages', () => { it('Should handle an Error thrown by next()', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware((c, next) => next()) const next = vi.fn().mockRejectedValue(new Error('Error from next()')) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await expect(handler({ request, env, next })).rejects.toThrowError('Error from next()') + const eventContext = createEventContext({ request, next }) + await expect(handler(eventContext)).rejects.toThrowError('Error from next()') expect(next).toHaveBeenCalled() }) it('Should handle a non-Error thrown by next()', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware((c, next) => next()) const next = vi.fn().mockRejectedValue('Error from next()') - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await expect(handler({ request, env, next })).rejects.toThrowError('Error from next()') + const eventContext = createEventContext({ request, next }) + await expect(handler(eventContext)).rejects.toThrowError('Error from next()') expect(next).toHaveBeenCalled() }) it('Should rethrow an Error', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware(() => { throw new Error('Something went wrong') }) const next = vi.fn() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await expect(handler({ request, env, next })).rejects.toThrowError('Something went wrong') + const eventContext = createEventContext({ request, next }) + await expect(handler(eventContext)).rejects.toThrowError('Something went wrong') expect(next).not.toHaveBeenCalled() }) it('Should rethrow non-Error exceptions', async () => { const request = new Request('http://localhost/api/foo') - const env = { - TOKEN: 'HONOISCOOL', - } const handler = handleMiddleware(() => Promise.reject('Something went wrong')) const next = vi.fn() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - await expect(handler({ request, env, next })).rejects.toThrowError('Something went wrong') + const eventContext = createEventContext({ request, next }) + await expect(handler(eventContext)).rejects.toThrowError('Something went wrong') expect(next).not.toHaveBeenCalled() }) }) + +describe('serveStatic()', () => { + it('Should pass the raw request to ASSETS.fetch', async () => { + const assetsFetch = vi.fn().mockResolvedValue(new Response('foo.png')) + const request = new Request('http://localhost/foo.png') + const env = { + ASSETS: { fetch: assetsFetch }, + TOKEN: 'HONOISCOOL', + } + + const eventContext = createEventContext({ request, env }) + const app = new Hono() + app.use(serveStatic()) + const handler = handle(app) + const res = await handler(eventContext) + + expect(assetsFetch).toHaveBeenCalledWith(request) + expect(res.status).toBe(200) + expect(await res.text()).toBe('foo.png') + }) + + it('Should respond with 404 if ASSETS.fetch returns a 404 response', async () => { + const assetsFetch = vi.fn().mockResolvedValue(new Response(null, { status: 404 })) + const request = new Request('http://localhost/foo.png') + const env = { + ASSETS: { fetch: assetsFetch }, + TOKEN: 'HONOISCOOL', + } + + const eventContext = createEventContext({ request, env }) + const app = new Hono() + app.use(serveStatic()) + const handler = handle(app) + const res = await handler(eventContext) + + expect(assetsFetch).toHaveBeenCalledWith(request) + expect(res.status).toBe(404) + }) +}) diff --git a/src/adapter/cloudflare-pages/handler.ts b/src/adapter/cloudflare-pages/handler.ts index 371175b8..a3b98857 100644 --- a/src/adapter/cloudflare-pages/handler.ts +++ b/src/adapter/cloudflare-pages/handler.ts @@ -1,7 +1,7 @@ import { Context } from '../../context' import type { Hono } from '../../hono' import { HTTPException } from '../../http-exception' -import type { Env, Input, MiddlewareHandler } from '../../types' +import type { BlankSchema, Env, Input, MiddlewareHandler, Schema } from '../../types' // Ref: https://github.com/cloudflare/workerd/blob/main/types/defines/pages.d.ts @@ -27,26 +27,25 @@ declare type PagesFunction< Data extends Record = Record > = (context: EventContext) => Response | Promise -interface HandleInterface { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (app: Hono): (eventContext: EventContext) => Response | Promise -} - -export const handle: HandleInterface = (app: Hono) => (eventContext: EventContext) => { - return app.fetch( - eventContext.request, - { ...eventContext.env, eventContext }, - { - waitUntil: eventContext.waitUntil, - passThroughOnException: eventContext.passThroughOnException, - } - ) -} +export const handle = + ( + app: Hono + ): PagesFunction => + (eventContext) => { + return app.fetch( + eventContext.request, + { ...eventContext.env, eventContext }, + { + waitUntil: eventContext.waitUntil, + passThroughOnException: eventContext.passThroughOnException, + } + ) + } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function handleMiddleware( middleware: MiddlewareHandler -): PagesFunction { +): PagesFunction { return async (executionCtx) => { const context = new Context(executionCtx.request, { env: executionCtx.env,