Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions UPGRADE-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,4 @@

# Event

- Remove `eventBusMiddleware` and use `httpKernel` instead.
- Replace all the types from `@koala-ts/framework/Event` with types from `@koala-ts/framework/Kernel`.

```diff
-- import { eventBusMiddleware } from '@koala-ts/framework/Event';
++ import { httpKernel } from '@koala-ts/framework/Kernel';
```
- Remove all usage of the `Event` module.
176 changes: 5 additions & 171 deletions src/Application/ApplicationFactory.test.ts
Original file line number Diff line number Diff line change
@@ -1,175 +1,9 @@
import Koa from 'koa';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { expect, test } from 'vitest';
import { create } from '@/Application/ApplicationFactory';
import { KoalaConfig, koalaDefaultConfig } from '@/Config';
import type { HttpRequest, HttpScope, NextMiddleware, UploadedFile } from '@/Http';
import { Route } from '@/Routing';
import { createTestAgent, TestAgent } from '@/Testing';
import { koalaDefaultConfig } from '@/Config';

describe('Application', () => {
let agent: TestAgent;
beforeEach(() => {
agent = createTestAgent(koalaDefaultConfig);
});
test('create app with default config', () => {
const app = create(koalaDefaultConfig);

const middleware1 = function (_: HttpScope, next: NextMiddleware): Promise<void> {
return next();
};
const service = vi.fn();

const middleware2 = function (_: HttpScope, next: NextMiddleware): Promise<void> {
service();
return next();
};

interface BarRequest extends HttpRequest {
body?: {
name: string;
};
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
class FooController {
@Route({ method: 'any', path: '/bar', options: { parseBody: false }, middleware: [middleware1, middleware2] })
bar(scope: HttpScope<BarRequest>): void {
scope.response.body = {
name: scope.request.body?.name ?? 'Koala',
};
}

@Route({ method: 'post', path: '/qux' })
qux(scope: HttpScope): void {
scope.response.body = scope.request.body;
}

@Route({ method: ['get', 'post'], path: '/handle-multiple-methods' })
handleMultipleMethods(scope: HttpScope): void {
scope.response.body = { method: scope.request.method };
}

@Route({ method: 'get', path: '/access-route-params/:id' })
accessRouteParams(scope: HttpScope): void {
scope.response.body = { id: scope.request.params.id };
}

@Route({ method: 'post', path: '/upload', options: { multipart: true } })
upload(scope: HttpScope): void {
const avatar = scope.request.files?.avatar as unknown as UploadedFile;
scope.response.body = avatar.originalFilename;
}

@Route({ method: 'get', path: '/set-header' })
setHeader(scope: HttpScope): void {
scope.response.setHeader('X-Foo', 'Bar').setHeader('X-Bar', 'Foo').body = 'Header set';
}

@Route({ method: 'get', path: '/with-headers' })
withHeaders(scope: HttpScope): void {
scope.response.withHeaders({
'X-Foo': 'Bar',
'X-Bar': 'Foo',
}).body = 'Headers set';
}
}

test('it creates application instance', () => {
const app = create(koalaDefaultConfig);

expect(app).toBeInstanceOf(Koa);
});

test('app provides access to scope', () => {
const app = create(koalaDefaultConfig);

expect(app.scope).toBe(app.context);
});

test('e2e set header', async () => {
const response = await agent.get('/set-header');

expect(response.header['x-foo']).toBe('Bar');
expect(response.header['x-bar']).toBe('Foo');
});

test('e2e with headers', async () => {
const response = await agent.get('/with-headers');

expect(response.header['x-foo']).toBe('Bar');
expect(response.header['x-bar']).toBe('Foo');
});

test('e2e with body', async () => {
const response = await agent.post('/qux').send({ name: 'Koala' });

expect(response.status).toBe(200);
expect(response.body).toEqual({ name: 'Koala' });
});

test('e2e without body', async () => {
const response = await agent.get('/bar').send({ name: 'Not Koala' });

expect(response.status).toBe(200);
expect(response.body).toEqual({ name: 'Koala' });
});

test('it should call middleware', async () => {
await agent.get('/bar');

expect(service).toHaveBeenCalled();
});

test('it should handle multiple methods', async () => {
const response1 = await agent.get('/handle-multiple-methods');
const response2 = await agent.post('/handle-multiple-methods');

expect(response1.status).toBe(200);
expect(response2.status).toBe(200);
expect(response1.body).toEqual({ method: 'GET' });
expect(response2.body).toEqual({ method: 'POST' });
});

test('it should access route params', async () => {
const id = '123';
const response = await agent.get(`/access-route-params/${id}`);

expect(response.status).toBe(200);
expect(response.body).toEqual({ id });
});

test('upload file', async () => {
const response = await agent.post('/upload').attach('avatar', 'tests/fixtures/avatar.png');

expect(response.text).toBe('avatar.png');
});
});

describe('Global Middleware', () => {
test('it should register configured global middleware', async () => {
const middlewareFn = vi.fn();
const config = {
controllers: [],
globalMiddleware: [middlewareFn],
};
const testAgent = createTestAgent(config as KoalaConfig);

await testAgent.get('/');

expect(middlewareFn).toHaveBeenCalled();
});
});

describe('Serving Static Files', () => {
test('it should serve public static files', async () => {
const config = {
staticFiles: {
root: 'tests/fixtures',
},
};
const testAgent = createTestAgent(config as KoalaConfig);

const response = await testAgent.get('/sample.txt');

expect(response.status).toBe(200);
expect(response.text).toBe('Howdy!\n');
});
expect(app).toBeDefined();
});
15 changes: 10 additions & 5 deletions src/Application/ApplicationFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import { extendResponse } from '@/Application/Response';
import { type KoalaConfig } from '@/Config';
import { type HttpMiddleware, type HttpScope } from '@/Http';
import { serveStaticFiles } from '@/Http/Files';
import { type EventSubscriber, httpKernel } from '@/Kernel';
import { getRoutes } from '@/Routing';

export function create(config: KoalaConfig): Application {
const app = new Koa() as Application;
app.scope = app.context;

app.use(extendResponse);
if (undefined !== config.globalMiddleware) registerGlobalMiddleware(app, config.globalMiddleware);
app.use(httpKernel);

if (undefined !== config.globalMiddleware) {
registerGlobalMiddleware(app, config.globalMiddleware);
}

app.use(serveStaticFiles(config.staticFiles));

Expand All @@ -22,7 +27,9 @@ export function create(config: KoalaConfig): Application {
app.use(router.routes());
app.use(router.allowedMethods());

registerEventSubscribers(app, config.eventSubscribers);
if (undefined !== config.eventSubscribers) {
registerEventSubscribers(app, config.eventSubscribers);
}

return app;
}
Expand All @@ -48,9 +55,7 @@ function registerGlobalMiddleware(app: Application, middleware: HttpMiddleware[]
}
}

function registerEventSubscribers(app: Application, map: KoalaConfig['eventSubscribers']): void {
if (undefined === map) return;

function registerEventSubscribers(app: Application, map: Record<string, EventSubscriber | EventSubscriber[]>): void {
for (const [event, subscribers] of Object.entries(map)) {
if (Array.isArray(subscribers)) {
for (const subscriber of subscribers) app.on(event, subscriber as unknown as (...args: unknown[]) => void);
Expand Down
20 changes: 16 additions & 4 deletions src/Config/ConfigLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ describe('Load env config', () => {
test('it should load .env file', () => {
loadEnvConfig('development');

expect(configSpy).toHaveBeenCalledWith({ path: expect.stringContaining('.env'), override: true });
expect(configSpy).toHaveBeenCalledWith({ path: expect.stringContaining('.env'), override: true, quiet: true });
expect(expandSpy).toHaveBeenCalled();
});

test('it should load .env.local file', () => {
loadEnvConfig('development');

expect(configSpy).toHaveBeenCalledWith({ path: expect.stringContaining('.env.local'), override: true });
expect(configSpy).toHaveBeenCalledWith({
path: expect.stringContaining('.env.local'),
override: true,
quiet: true,
});
expect(expandSpy).toHaveBeenCalled();
});

Expand All @@ -30,14 +34,22 @@ describe('Load env config', () => {
test('it should load .env.<env> file', () => {
loadEnvConfig('development');

expect(configSpy).toHaveBeenCalledWith({ path: expect.stringContaining('.env.development'), override: true });
expect(configSpy).toHaveBeenCalledWith({
path: expect.stringContaining('.env.development'),
override: true,
quiet: true,
});
expect(expandSpy).toHaveBeenCalled();
});

test('it should load .env.<env>.local file', () => {
loadEnvConfig('test');

expect(configSpy).toHaveBeenCalledWith({ path: expect.stringContaining('.env.test.local'), override: true });
expect(configSpy).toHaveBeenCalledWith({
path: expect.stringContaining('.env.test.local'),
override: true,
quiet: true,
});
expect(expandSpy).toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions src/Config/ConfigLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function loadEnvFile(fileName: string): void {
const expandedOptions = dotenv.config({
path: path.resolve(rootDir, fileName),
override: true,
quiet: true,
});

dotenvExpand.expand(expandedOptions);
Expand Down
Empty file removed tests/.gitignore
Empty file.
53 changes: 0 additions & 53 deletions tests/RegisterEventSubscribers.test.ts

This file was deleted.

54 changes: 54 additions & 0 deletions tests/event-subscribers.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, test, vi } from 'vitest';
import { createTestAgent, KoalaConfig, Route } from '../src';
import { useEmit } from '../src/Kernel';

class MyController {
@Route({ method: 'ANY', path: '/publish-event' })
publishEvent(): void {
const emitter = useEmit();
emitter.emit('myEvent', { data: 'event-data' });
}
}

describe('Event subscribers E2E Test', () => {
test('subscribe to an event', async () => {
const handler = vi.fn();
const agent = createTestAgent({
controllers: [MyController],
eventSubscribers: { myEvent: handler },
} as unknown as KoalaConfig);

await agent.get('/publish-event');

expect(handler).toHaveBeenCalledWith({ data: 'event-data' });
});

test('multiple subscribers to an event', async () => {
const handler1 = vi.fn();
const handler2 = vi.fn();
const agent = createTestAgent({
controllers: [MyController],
eventSubscribers: { myEvent: [handler1, handler2] },
} as unknown as KoalaConfig);

await agent.get('/publish-event');

expect(handler1).toHaveBeenCalledWith({ data: 'event-data' });
expect(handler2).toHaveBeenCalledWith({ data: 'event-data' });
});

test('register async event subscriber', async () => {
const internalFn = vi.fn();
const asyncSubscriber = async (data: string): Promise<void> => {
await internalFn(data);
};
const agent = createTestAgent({
controllers: [MyController],
eventSubscribers: { myEvent: asyncSubscriber },
} as unknown as KoalaConfig);

await agent.get('/publish-event');

expect(internalFn).toHaveBeenCalledWith({ data: 'event-data' });
});
});
Loading