Files
http-kernel/src/Utils/__tests__/createRouteMatcher.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

119 lines
3.9 KiB
TypeScript

import {
assert,
assertEquals,
assertStrictEquals,
} from 'https://deno.land/std/assert/mod.ts';
import { IRouteDefinition } from '../../Interfaces/mod.ts';
import { createRouteMatcher } from '../../mod.ts';
// Dummy request
const dummyRequest = new Request('http://localhost');
Deno.test('createRouteMatcher: static route matches and extracts params', () => {
const def: IRouteDefinition = { method: 'GET', path: '/users/:id' };
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/users/42'), dummyRequest);
assert(result);
assertEquals(result.params, { id: '42' });
});
Deno.test('createRouteMatcher: static route with multiple params', () => {
const def: IRouteDefinition = { method: 'GET', path: '/repo/:owner/:name' };
const matcher = createRouteMatcher(def);
const result = matcher(
new URL('http://localhost/repo/max/wiki'),
dummyRequest,
);
assert(result);
assertEquals(result.params, { owner: 'max', name: 'wiki' });
});
Deno.test('createRouteMatcher: static route does not match wrong path', () => {
const def: IRouteDefinition = { method: 'GET', path: '/users/:id' };
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/posts/42'), dummyRequest);
assertStrictEquals(result, null);
});
Deno.test('createRouteMatcher: uses custom matcher if provided', () => {
const def: IRouteDefinition = {
method: 'GET',
matcher: (url) => url.pathname === '/ping' ? { params: {} } : null,
};
const matcher = createRouteMatcher(def);
const result = matcher(new URL('http://localhost/ping'), dummyRequest);
assert(result);
assertEquals(result.params, {});
});
Deno.test('createRouteMatcher: extracts single query param', () => {
const def: IRouteDefinition = { method: 'GET', path: '/search' };
const matcher = createRouteMatcher(def);
const url = new URL('http://localhost/search?q=deno');
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.params, {}); // no path params
assertEquals(result.query, { q: 'deno' }); // single key → string
});
Deno.test('createRouteMatcher: duplicate query keys become array', () => {
const def: IRouteDefinition = { method: 'GET', path: '/tags' };
const matcher = createRouteMatcher(def);
const url = new URL('http://localhost/tags?tag=js&tag=ts&tag=deno');
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.params, {});
assertEquals(result.query, { tag: ['js', 'ts', 'deno'] }); // multi → string[]
});
Deno.test('createRouteMatcher: mix of single and duplicate keys', () => {
const def: IRouteDefinition = { method: 'GET', path: '/filter/:type' };
const matcher = createRouteMatcher(def);
const url = new URL('http://localhost/filter/repo?lang=ts&lang=js&page=2');
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.params, { type: 'repo' });
assertEquals(result.query, {
lang: ['ts', 'js'], // duplicated
page: '2', // single
});
});
Deno.test('createRouteMatcher: no query parameters returns empty object', () => {
const def: IRouteDefinition = { method: 'GET', path: '/info' };
const matcher = createRouteMatcher(def);
const url = new URL('http://localhost/info');
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.params, {});
assertEquals(result.query, {}); // empty
});
Deno.test('createRouteMatcher: retains array order of duplicate keys', () => {
const def: IRouteDefinition = { method: 'GET', path: '/order' };
const matcher = createRouteMatcher(def);
const url = new URL(
'http://localhost/order?item=first&item=second&item=third',
);
const result = matcher(url, dummyRequest);
assert(result);
assertEquals(result.query?.item, ['first', 'second', 'third']);
});