Files
http-kernel/src/__tests__/HttpKernel.test.ts
Max P. b7410b44dd refactor(core): enhance HttpKernel pipeline and matcher system with full context and error handling
BREAKING CHANGE: `parseQuery` utility removed; `IRouteMatcher` now includes query parsing; `RouteBuilder.middleware` and `handle` are now strictly typed per builder instance.

- Add `isHandler` and `isMiddleware` runtime type guards for validation in `HttpKernel`.
- Introduce `createEmptyContext` for constructing default context objects.
- Support custom HTTP error handlers (`404`, `500`) via `IHttpKernelConfig.httpErrorHandlers`.
- Default error handlers return meaningful HTTP status text (e.g., "Not Found").
- Replace legacy `parseQuery` logic with integrated query extraction via `createRouteMatcher`.

- Strongly type `RouteBuilder.middleware()` and `.handle()` methods without generic overrides.
- Simplify `HttpKernel.handle()` and `executePipeline()` through precise control flow and validation.
- Remove deprecated `registerRoute.ts` and `HttpKernelConfig.ts` in favor of colocated type exports.

- Add tests for integrated query parsing in `createRouteMatcher`.
- Improve error handling tests: middleware/handler validation, double `next()` call, thrown exceptions.
- Replace `assertRejects` with plain response code checks (via updated error handling).

- Removed `parseQuery.ts` and all related tests — query parsing is now built into route matching.
- `IRouteMatcher` signature changed to return `{ params, query }` instead of only `params`.
- `HttpKernelConfig` now uses `DeepPartial` and includes `httpErrorHandlers`.
- `RouteBuilder`'s generics are simplified for better DX and improved type safety.

This refactor improves clarity, test coverage, and runtime safety of the request lifecycle while reducing boilerplate and eliminating duplicated query handling logic.

Signed-off-by: Max P. <Mail@MPassarello.de>
2025-05-07 16:45:10 +02:00

181 lines
6.1 KiB
TypeScript

import { assertEquals } from 'https://deno.land/std@0.204.0/assert/mod.ts';
import { HttpKernel } from '../HttpKernel.ts';
import { IRouteDefinition } from '../Interfaces/mod.ts';
Deno.test('HttpKernel: matches static route and executes handler', async () => {
const kernel = new HttpKernel();
const def: IRouteDefinition = { method: 'GET', path: '/hello' };
let called = false;
kernel.route(def).handle((_ctx) => {
called = true;
return Promise.resolve(new Response('OK', { status: 200 }));
});
const res = await kernel.handle(
new Request('http://localhost/hello', { method: 'GET' }),
);
assertEquals(res.status, 200);
assertEquals(await res.text(), 'OK');
assertEquals(called, true);
});
Deno.test('HttpKernel: supports dynamic matcher', async () => {
const kernel = new HttpKernel();
const def: IRouteDefinition = {
method: 'GET',
matcher: (url) => url.pathname === '/dyn' ? { params: {} } : null,
};
kernel.route(def).handle((_ctx) =>
Promise.resolve(new Response('Dyn', { status: 200 }))
);
const res = await kernel.handle(new Request('http://localhost/dyn'));
assertEquals(res.status, 200);
assertEquals(await res.text(), 'Dyn');
});
Deno.test('HttpKernel: calls middleware in order and passes to handler', async () => {
const kernel = new HttpKernel();
const calls: string[] = [];
kernel.route({ method: 'GET', path: '/test' })
.middleware(async (_ctx, next) => {
calls.push('mw1');
return await next();
})
.middleware(async (_ctx, next) => {
calls.push('mw2');
return await next();
})
.handle((_ctx) => {
calls.push('handler');
return Promise.resolve(new Response('done'));
});
const res = await kernel.handle(
new Request('http://localhost/test', { method: 'GET' }),
);
assertEquals(await res.text(), 'done');
assertEquals(calls, ['mw1', 'mw2', 'handler']);
});
Deno.test('HttpKernel: middleware short-circuits pipeline', async () => {
const kernel = new HttpKernel();
const calls: string[] = [];
kernel.route({ method: 'GET', path: '/stop' })
.middleware((_ctx, _next) => {
calls.push('mw1');
return Promise.resolve(new Response('blocked', { status: 403 }));
})
.middleware((_ctx, _next) => {
calls.push('mw2');
return Promise.resolve(new Response('should-not-call'));
})
.handle((_ctx) => {
calls.push('handler');
return Promise.resolve(new Response('ok'));
});
const res = await kernel.handle(
new Request('http://localhost/stop', { method: 'GET' }),
);
assertEquals(res.status, 403);
assertEquals(await res.text(), 'blocked');
assertEquals(calls, ['mw1']);
});
Deno.test('HttpKernel: invalid middleware or handler signature triggers 500', async () => {
const kernel = new HttpKernel();
// Middleware with wrong signature (missing ctx, next)
kernel.route({ method: 'GET', path: '/bad-mw' })
// @ts-expect-error invalid middleware
.middleware(() => new Response('invalid'))
.handle((_ctx) => Promise.resolve(new Response('ok')));
const res1 = await kernel.handle(new Request('http://localhost/bad-mw'));
assertEquals(res1.status, 500);
assertEquals(await res1.text(), 'Internal Server Error');
// Handler with wrong signature (no ctx)
kernel.route({ method: 'GET', path: '/bad-handler' })
.middleware(async (_ctx, next) => await next())
// @ts-expect-error invalid handler
.handle(() => new Response('invalid'));
const res2 = await kernel.handle(
new Request('http://localhost/bad-handler'),
);
assertEquals(res2.status, 500);
assertEquals(await res2.text(), 'Internal Server Error');
});
Deno.test('HttpKernel: 404 for unmatched route', async () => {
const kernel = new HttpKernel();
const res = await kernel.handle(new Request('http://localhost/nothing'));
assertEquals(res.status, 404);
});
Deno.test('HttpKernel: skips route with wrong method', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'POST', path: '/only-post' })
.handle(() => Promise.resolve(new Response('nope')));
const res = await kernel.handle(
new Request('http://localhost/only-post', { method: 'GET' }),
);
assertEquals(res.status, 404);
});
Deno.test('HttpKernel: throws on next() called twice', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/bad' })
.middleware(async (_ctx, next) => {
await next();
await next(); // ❌
return new Response('should never reach');
})
.handle((_ctx) => Promise.resolve(new Response('OK')));
const res = await kernel.handle(new Request('http://localhost/bad'));
assertEquals(res.status, 500);
assertEquals(await res.text(), 'Internal Server Error');
});
Deno.test('HttpKernel: handler throws → error propagates', async () => {
const kernel = new HttpKernel();
kernel.route({ method: 'GET', path: '/throw' })
.handle(() => {
throw new Error('fail!');
});
const res = await kernel.handle(new Request('http://localhost/throw'));
assertEquals(res.status, 500);
assertEquals(await res.text(), 'Internal Server Error');
});
Deno.test('HttpKernel: returns 500 if no handler or middleware defined', async () => {
const kernel = new HttpKernel();
// Force-manual Registrierung mit `handler: undefined`
// Umgehen des Builders zur Simulation dieses Edge-Cases
kernel['routes'].push({
method: 'GET',
matcher: (url) => url.pathname === '/fail' ? { params: {} } : null,
middlewares: [],
// @ts-expect-error absichtlich ungültiger Handler
handler: undefined,
});
const res = await kernel.handle(new Request('http://localhost/fail'));
assertEquals(res.status, 500);
assertEquals(await res.text(), 'Internal Server Error');
});