@martel/calyx 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/src/config/config.module.ts +61 -0
  4. package/src/config/config.service.ts +24 -0
  5. package/src/config/index.ts +2 -0
  6. package/src/event-emitter/decorators.ts +10 -0
  7. package/src/event-emitter/event-emitter.module.ts +17 -0
  8. package/src/event-emitter/event-emitter.ts +61 -0
  9. package/src/event-emitter/index.ts +3 -0
  10. package/src/http/application.ts +336 -3
  11. package/src/http/factory.ts +7 -0
  12. package/src/index.ts +6 -0
  13. package/src/lifecycle/context.ts +75 -0
  14. package/src/lifecycle/interfaces.ts +3 -0
  15. package/src/microservices/client-proxy.ts +7 -0
  16. package/src/microservices/client-tcp.ts +127 -0
  17. package/src/microservices/decorators.ts +19 -0
  18. package/src/microservices/index.ts +6 -0
  19. package/src/microservices/interfaces.ts +11 -0
  20. package/src/microservices/microservice.ts +114 -0
  21. package/src/microservices/server-tcp.ts +210 -0
  22. package/src/schedule/cron.matcher.ts +45 -0
  23. package/src/schedule/decorators.ts +28 -0
  24. package/src/schedule/index.ts +3 -0
  25. package/src/schedule/schedule.module.ts +13 -0
  26. package/src/security/cors.middleware.ts +50 -0
  27. package/src/security/hashing.service.ts +12 -0
  28. package/src/security/helmet.middleware.ts +45 -0
  29. package/src/security/index.ts +3 -0
  30. package/src/websockets/decorators.ts +49 -0
  31. package/src/websockets/gateway.ts +11 -0
  32. package/src/websockets/index.ts +2 -0
  33. package/tests/config-event.test.ts +145 -0
  34. package/tests/microservices.test.ts +105 -0
  35. package/tests/rpc-ws-context.test.ts +135 -0
  36. package/tests/schedule.test.ts +64 -0
  37. package/tests/security.test.ts +89 -0
  38. package/tests/websockets.test.ts +125 -0
@@ -0,0 +1,13 @@
1
+ import { Module, DynamicModule } from '../core/decorators.ts';
2
+
3
+ @Module({})
4
+ export class ScheduleModule {
5
+ static forRoot(): DynamicModule {
6
+ return {
7
+ module: ScheduleModule,
8
+ providers: [],
9
+ exports: [],
10
+ global: true,
11
+ };
12
+ }
13
+ }
@@ -0,0 +1,50 @@
1
+ import { CalyxResponse } from '../http/application.ts';
2
+
3
+ export interface CorsOptions {
4
+ origin?: string | string[] | boolean;
5
+ methods?: string | string[];
6
+ allowedHeaders?: string | string[];
7
+ exposedHeaders?: string | string[];
8
+ credentials?: boolean;
9
+ maxAge?: number;
10
+ }
11
+
12
+ export function cors(options: CorsOptions = {}) {
13
+ const origin = options.origin ?? '*';
14
+ const methods = options.methods ?? 'GET,HEAD,PUT,PATCH,POST,DELETE';
15
+ const allowedHeaders = options.allowedHeaders ?? '*';
16
+ const credentials = options.credentials ?? false;
17
+ const maxAge = options.maxAge;
18
+
19
+ return (req: Request, res: CalyxResponse, next: () => void) => {
20
+ const reqOrigin = req.headers.get('origin');
21
+
22
+ if (origin === '*') {
23
+ res.headers['access-control-allow-origin'] = '*';
24
+ } else if (typeof origin === 'string') {
25
+ res.headers['access-control-allow-origin'] = origin;
26
+ } else if (Array.isArray(origin) && reqOrigin && origin.includes(reqOrigin)) {
27
+ res.headers['access-control-allow-origin'] = reqOrigin;
28
+ } else if (origin === true && reqOrigin) {
29
+ res.headers['access-control-allow-origin'] = reqOrigin;
30
+ }
31
+
32
+ res.headers['access-control-allow-methods'] = Array.isArray(methods) ? methods.join(',') : methods;
33
+ res.headers['access-control-allow-headers'] = Array.isArray(allowedHeaders) ? allowedHeaders.join(',') : allowedHeaders;
34
+
35
+ if (credentials) {
36
+ res.headers['access-control-allow-credentials'] = 'true';
37
+ }
38
+
39
+ if (maxAge !== undefined) {
40
+ res.headers['access-control-max-age'] = String(maxAge);
41
+ }
42
+
43
+ if (req.method === 'OPTIONS') {
44
+ res.status(204).send('');
45
+ return;
46
+ }
47
+
48
+ next();
49
+ };
50
+ }
@@ -0,0 +1,12 @@
1
+ import { Injectable } from '../core/decorators.ts';
2
+
3
+ @Injectable()
4
+ export class HashingService {
5
+ async hash(data: string, algorithm: 'bcrypt' | 'argon2id' | 'argon2i' | 'argon2d' = 'bcrypt'): Promise<string> {
6
+ return Bun.password.hash(data, algorithm);
7
+ }
8
+
9
+ async verify(data: string, hash: string): Promise<boolean> {
10
+ return Bun.password.verify(data, hash);
11
+ }
12
+ }
@@ -0,0 +1,45 @@
1
+ import { CalyxResponse } from '../http/application.ts';
2
+
3
+ export interface HelmetOptions {
4
+ contentSecurityPolicy?: boolean | string;
5
+ crossOriginOpenerPolicy?: string;
6
+ crossOriginResourcePolicy?: string;
7
+ referrerPolicy?: string;
8
+ xContentTypeOptions?: string;
9
+ xDnsPrefetchControl?: string;
10
+ xFrameOptions?: string;
11
+ xXssProtection?: string;
12
+ }
13
+
14
+ export function helmet(options: HelmetOptions = {}) {
15
+ const headers: Record<string, string> = {
16
+ 'cross-origin-opener-policy': options.crossOriginOpenerPolicy ?? 'same-origin',
17
+ 'cross-origin-resource-policy': options.crossOriginResourcePolicy ?? 'same-origin',
18
+ 'origin-agent-cluster': '?1',
19
+ 'referrer-policy': options.referrerPolicy ?? 'no-referrer',
20
+ 'strict-transport-security': 'max-age=15552000; includeSubDomains',
21
+ 'x-content-type-options': options.xContentTypeOptions ?? 'nosniff',
22
+ 'x-dns-prefetch-control': options.xDnsPrefetchControl ?? 'off',
23
+ 'x-download-options': 'noopen',
24
+ 'x-frame-options': options.xFrameOptions ?? 'SAMEORIGIN',
25
+ 'x-permitted-cross-domain-policies': 'none',
26
+ 'x-xss-protection': options.xXssProtection ?? '0',
27
+ };
28
+
29
+ const csp = options.contentSecurityPolicy;
30
+ if (csp !== false) {
31
+ headers['content-security-policy'] = typeof csp === 'string'
32
+ ? csp
33
+ : "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests";
34
+ }
35
+
36
+ const headerPairs = Object.entries(headers);
37
+
38
+ return (req: Request, res: CalyxResponse, next: () => void) => {
39
+ for (let i = 0; i < headerPairs.length; i++) {
40
+ const pair = headerPairs[i];
41
+ res.headers[pair[0]] = pair[1];
42
+ }
43
+ next();
44
+ };
45
+ }
@@ -0,0 +1,3 @@
1
+ export * from './hashing.service.ts';
2
+ export * from './cors.middleware.ts';
3
+ export * from './helmet.middleware.ts';
@@ -0,0 +1,49 @@
1
+ import 'reflect-metadata';
2
+
3
+ export interface GatewayMetadata {
4
+ port?: number;
5
+ namespace?: string;
6
+ cors?: any;
7
+ }
8
+
9
+ export function WebSocketGateway(portOrOptions?: number | GatewayMetadata): ClassDecorator {
10
+ return (target) => {
11
+ let metadata: GatewayMetadata = {};
12
+ if (typeof portOrOptions === 'number') {
13
+ metadata.port = portOrOptions;
14
+ } else if (portOrOptions) {
15
+ metadata = portOrOptions;
16
+ }
17
+ Reflect.defineMetadata('calyx:websocket_gateway', metadata, target);
18
+ Reflect.defineMetadata('calyx:injectable', true, target);
19
+ };
20
+ }
21
+
22
+ export function SubscribeMessage(event: string): MethodDecorator {
23
+ return (target, propertyKey) => {
24
+ const constructor = target.constructor;
25
+ const existing = Reflect.getOwnMetadata('calyx:subscribe_message', constructor) || [];
26
+ existing.push({ event, propertyKey });
27
+ Reflect.defineMetadata('calyx:subscribe_message', existing, constructor);
28
+ };
29
+ }
30
+
31
+ export function MessageBody(): ParameterDecorator {
32
+ return (target, propertyKey, parameterIndex) => {
33
+ if (!propertyKey) return;
34
+ const constructor = target.constructor;
35
+ const existing = Reflect.getOwnMetadata('calyx:message_body', constructor) || [];
36
+ existing.push({ propertyKey, parameterIndex });
37
+ Reflect.defineMetadata('calyx:message_body', existing, constructor);
38
+ };
39
+ }
40
+
41
+ export function ConnectedSocket(): ParameterDecorator {
42
+ return (target, propertyKey, parameterIndex) => {
43
+ if (!propertyKey) return;
44
+ const constructor = target.constructor;
45
+ const existing = Reflect.getOwnMetadata('calyx:connected_socket', constructor) || [];
46
+ existing.push({ propertyKey, parameterIndex });
47
+ Reflect.defineMetadata('calyx:connected_socket', existing, constructor);
48
+ };
49
+ }
@@ -0,0 +1,11 @@
1
+ export interface OnGatewayInit {
2
+ afterInit(server: any): void;
3
+ }
4
+
5
+ export interface OnGatewayConnection {
6
+ handleConnection(client: any, ...args: any[]): void;
7
+ }
8
+
9
+ export interface OnGatewayDisconnect {
10
+ handleDisconnect(client: any): void;
11
+ }
@@ -0,0 +1,2 @@
1
+ export * from './decorators.ts';
2
+ export * from './gateway.ts';
@@ -0,0 +1,145 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
2
+ import {
3
+ Module,
4
+ Controller,
5
+ Get,
6
+ Injectable,
7
+ CalyxFactory,
8
+ ConfigModule,
9
+ ConfigService,
10
+ EventEmitterModule,
11
+ EventEmitter,
12
+ OnEvent,
13
+ } from '../src/index.ts';
14
+
15
+ // 1. Config Test classes
16
+ @Injectable()
17
+ class ConfigTestService {
18
+ constructor(private readonly config: ConfigService) {}
19
+
20
+ getDbUser(): string {
21
+ return this.config.get('DB_USER', 'default');
22
+ }
23
+
24
+ getDbPort(): number {
25
+ return this.config.get('DB_PORT', 5432);
26
+ }
27
+
28
+ getDebugMode(): boolean {
29
+ return this.config.get('DEBUG_MODE', false);
30
+ }
31
+ }
32
+
33
+ // 2. Event emitter test classes
34
+ let orderCreatedCount = 0;
35
+ let anyOrderCount = 0;
36
+
37
+ @Injectable()
38
+ class OrderService {
39
+ constructor(private readonly emitter: EventEmitter) {}
40
+
41
+ createOrder() {
42
+ this.emitter.emit('order.created', { id: 101, total: 50.0 });
43
+ }
44
+
45
+ cancelOrder() {
46
+ this.emitter.emit('order.cancelled', { id: 101 });
47
+ }
48
+ }
49
+
50
+ @Injectable()
51
+ class NotificationService {
52
+ @OnEvent('order.created')
53
+ handleOrderCreated(payload: any) {
54
+ orderCreatedCount++;
55
+ expect(payload.id).toBe(101);
56
+ }
57
+
58
+ @OnEvent('order.*')
59
+ handleAnyOrder(payload: any) {
60
+ anyOrderCount++;
61
+ }
62
+ }
63
+
64
+ @Controller('test')
65
+ class TestController {
66
+ constructor(
67
+ private readonly configTest: ConfigTestService,
68
+ private readonly orders: OrderService
69
+ ) {}
70
+
71
+ @Get('config')
72
+ getConfig() {
73
+ return {
74
+ user: this.configTest.getDbUser(),
75
+ port: this.configTest.getDbPort(),
76
+ debug: this.configTest.getDebugMode(),
77
+ };
78
+ }
79
+
80
+ @Get('trigger-event')
81
+ triggerEvent() {
82
+ this.orders.createOrder();
83
+ this.orders.cancelOrder();
84
+ return { triggered: true };
85
+ }
86
+ }
87
+
88
+ @Module({
89
+ imports: [
90
+ ConfigModule.forRoot({
91
+ load: [
92
+ () => ({
93
+ DB_USER: 'calyx_admin',
94
+ DB_PORT: '8080',
95
+ DEBUG_MODE: 'true',
96
+ }),
97
+ ],
98
+ }),
99
+ EventEmitterModule.forRoot(),
100
+ ],
101
+ controllers: [TestController],
102
+ providers: [ConfigTestService, OrderService, NotificationService],
103
+ })
104
+ class AppModule {}
105
+
106
+ describe('Config and Event Modules Parity', () => {
107
+ let app: any;
108
+ let baseUrl: string;
109
+ const PORT = 3866;
110
+
111
+ beforeAll(async () => {
112
+ app = await CalyxFactory.create(AppModule);
113
+ await app.listen(PORT);
114
+ baseUrl = `http://localhost:${PORT}`;
115
+ });
116
+
117
+ afterAll(async () => {
118
+ await app.close();
119
+ });
120
+
121
+ test('should resolve custom environment variables via ConfigService', async () => {
122
+ const res = await fetch(`${baseUrl}/test/config`);
123
+ expect(res.status).toBe(200);
124
+ const body = await res.json();
125
+ expect(body).toEqual({
126
+ user: 'calyx_admin',
127
+ port: 8080,
128
+ debug: true,
129
+ });
130
+ });
131
+
132
+ test('should trigger event and notify OnEvent listeners, including wildcards', async () => {
133
+ orderCreatedCount = 0;
134
+ anyOrderCount = 0;
135
+
136
+ const res = await fetch(`${baseUrl}/test/trigger-event`);
137
+ expect(res.status).toBe(200);
138
+
139
+ // Wait for event loop ticks
140
+ await new Promise(resolve => setTimeout(resolve, 50));
141
+
142
+ expect(orderCreatedCount).toBe(1);
143
+ expect(anyOrderCount).toBe(2); // Matches order.created and order.cancelled
144
+ });
145
+ });
@@ -0,0 +1,105 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
2
+ import { of, Observable } from 'rxjs';
3
+ import {
4
+ Module,
5
+ Controller,
6
+ CalyxFactory,
7
+ MessagePattern,
8
+ EventPattern,
9
+ ClientTcp,
10
+ Transport,
11
+ } from '../src/index.ts';
12
+
13
+ let eventReceived = false;
14
+ let eventPayload: any = null;
15
+
16
+ @Controller()
17
+ class MathController {
18
+ @MessagePattern('sum')
19
+ accumulate(data: number[]): number {
20
+ return (data || []).reduce((a, b) => a + b, 0);
21
+ }
22
+
23
+ @MessagePattern('stream')
24
+ streamData(data: string): Observable<string> {
25
+ return of(`echo1: ${data}`, `echo2: ${data}`);
26
+ }
27
+
28
+ @MessagePattern('error_test')
29
+ throwError(data: any) {
30
+ throw new Error('This is a simulated RPC error');
31
+ }
32
+
33
+ @EventPattern('log_event')
34
+ handleLogEvent(data: any) {
35
+ eventReceived = true;
36
+ eventPayload = data;
37
+ }
38
+ }
39
+
40
+ @Module({
41
+ controllers: [MathController],
42
+ })
43
+ class MicroserviceModule {}
44
+
45
+ describe('TCP Microservices (ClientTcp, ServerTcp, MessagePattern, EventPattern)', () => {
46
+ let server: any;
47
+ let client: ClientTcp;
48
+ const PORT = 3899;
49
+
50
+ beforeAll(async () => {
51
+ // 1. Create and start microservice server
52
+ server = await CalyxFactory.createMicroservice(MicroserviceModule, {
53
+ transport: Transport.TCP,
54
+ options: { port: PORT },
55
+ });
56
+ await server.listen();
57
+
58
+ // 2. Initialize client
59
+ client = new ClientTcp({ port: PORT });
60
+ });
61
+
62
+ afterAll(async () => {
63
+ client.close();
64
+ await server.close();
65
+ });
66
+
67
+ test('should exchange request-response message and calculate sum', async () => {
68
+ const res = await client.send<number, number[]>('sum', [1, 2, 3, 4]).toPromise();
69
+ expect(res).toBe(10);
70
+ });
71
+
72
+ test('should handle streaming response from RxJS Observable', async () => {
73
+ const results: string[] = [];
74
+ await new Promise<void>((resolve, reject) => {
75
+ client.send<string, string>('stream', 'hello').subscribe({
76
+ next: (val) => results.push(val),
77
+ error: (err) => reject(err),
78
+ complete: () => resolve(),
79
+ });
80
+ });
81
+ expect(results).toEqual(['echo1: hello', 'echo2: hello']);
82
+ });
83
+
84
+ test('should propagate error message from RPC back to client', async () => {
85
+ try {
86
+ await client.send('error_test', {}).toPromise();
87
+ expect().fail('Expected send to throw error');
88
+ } catch (err: any) {
89
+ expect(err.message).toBe('This is a simulated RPC error');
90
+ }
91
+ });
92
+
93
+ test('should dispatch event pattern messages asynchronously', async () => {
94
+ eventReceived = false;
95
+ eventPayload = null;
96
+
97
+ await client.emit('log_event', { status: 'ok', code: 200 }).toPromise();
98
+
99
+ // Wait a tick for TCP event handling
100
+ await new Promise((resolve) => setTimeout(resolve, 50));
101
+
102
+ expect(eventReceived).toBe(true);
103
+ expect(eventPayload).toEqual({ status: 'ok', code: 200 });
104
+ });
105
+ });
@@ -0,0 +1,135 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
2
+ import { of, Observable } from 'rxjs';
3
+ import {
4
+ Module,
5
+ CalyxFactory,
6
+ WebSocketGateway,
7
+ SubscribeMessage,
8
+ MessageBody,
9
+ UseGuards,
10
+ UseInterceptors,
11
+ CanActivate,
12
+ ExecutionContext,
13
+ NestInterceptor,
14
+ CallHandler,
15
+ ClientTcp,
16
+ Transport,
17
+ MessagePattern,
18
+ Controller,
19
+ } from '../src/index.ts';
20
+
21
+ class SecurityGuard implements CanActivate {
22
+ canActivate(context: ExecutionContext): boolean {
23
+ const type = context.getType();
24
+ if (type === 'ws') {
25
+ const data = context.switchToWs().getData();
26
+ return data.user === 'admin';
27
+ }
28
+ if (type === 'rpc') {
29
+ const data = context.switchToRpc().getData();
30
+ return data.role === 'admin';
31
+ }
32
+ return false;
33
+ }
34
+ }
35
+
36
+ class LoggingInterceptor implements NestInterceptor {
37
+ async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {
38
+ const type = context.getType();
39
+ const result = await next.handle();
40
+ return { type, original: result };
41
+ }
42
+ }
43
+
44
+ @WebSocketGateway(3915)
45
+ class SecureGateway {
46
+ @SubscribeMessage('ping')
47
+ @UseGuards(SecurityGuard)
48
+ @UseInterceptors(LoggingInterceptor)
49
+ handlePing(@MessageBody() data: any) {
50
+ return 'pong';
51
+ }
52
+ }
53
+
54
+ @Controller()
55
+ class SecureController {
56
+ @MessagePattern('ping_rpc')
57
+ @UseGuards(SecurityGuard)
58
+ @UseInterceptors(LoggingInterceptor)
59
+ handlePingRpc(data: any) {
60
+ return 'pong_rpc';
61
+ }
62
+ }
63
+
64
+ @Module({
65
+ controllers: [SecureController],
66
+ providers: [SecureGateway],
67
+ })
68
+ class TestApp {}
69
+
70
+ describe('Guards and Interceptors for WS and RPC contexts', () => {
71
+ let wsApp: any;
72
+ let rpcApp: any;
73
+ let client: ClientTcp;
74
+ const PORT = 3889;
75
+ const RPC_PORT = 3916;
76
+
77
+ beforeAll(async () => {
78
+ // 1. Start standard app for HTTP/WS
79
+ wsApp = await CalyxFactory.create(TestApp);
80
+ await wsApp.listen(PORT);
81
+
82
+ // 2. Start dedicated microservice for RPC
83
+ rpcApp = await CalyxFactory.createMicroservice(TestApp, {
84
+ transport: Transport.TCP,
85
+ options: { port: RPC_PORT },
86
+ });
87
+ await rpcApp.listen();
88
+
89
+ client = new ClientTcp({ port: RPC_PORT });
90
+ });
91
+
92
+ afterAll(async () => {
93
+ client.close();
94
+ await rpcApp.close();
95
+ await wsApp.close();
96
+ });
97
+
98
+ test('should pass or deny WS events using context switching in guards/interceptors', async () => {
99
+ const ws = new WebSocket('ws://localhost:3915');
100
+
101
+ await new Promise<void>((resolve, reject) => {
102
+ ws.onopen = () => resolve();
103
+ ws.onerror = (err) => reject(err);
104
+ });
105
+
106
+ ws.send(JSON.stringify({ event: 'ping', data: { user: 'admin' } }));
107
+ const successRes = await new Promise<any>((resolve) => {
108
+ ws.onmessage = (event) => resolve(JSON.parse(event.data));
109
+ });
110
+ expect(successRes).toEqual({ type: 'ws', original: 'pong' });
111
+
112
+ let receivedMessage = false;
113
+ ws.onmessage = () => {
114
+ receivedMessage = true;
115
+ };
116
+ ws.send(JSON.stringify({ event: 'ping', data: { user: 'guest' } }));
117
+
118
+ await new Promise((resolve) => setTimeout(resolve, 100));
119
+ expect(receivedMessage).toBe(false);
120
+
121
+ ws.close();
122
+ });
123
+
124
+ test('should pass or deny RPC messages using context switching in guards/interceptors', async () => {
125
+ const successRes = await client.send('ping_rpc', { role: 'admin' }).toPromise();
126
+ expect(successRes).toEqual({ type: 'rpc', original: 'pong_rpc' });
127
+
128
+ try {
129
+ await client.send('ping_rpc', { role: 'guest' }).toPromise();
130
+ expect().fail('Expected send to throw error');
131
+ } catch (err: any) {
132
+ expect(err.message).toBe('Forbidden resource');
133
+ }
134
+ });
135
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
2
+ import {
3
+ Module,
4
+ CalyxFactory,
5
+ ScheduleModule,
6
+ Cron,
7
+ Interval,
8
+ Timeout,
9
+ Injectable,
10
+ } from '../src/index.ts';
11
+
12
+ let cronCalls = 0;
13
+ let intervalCalls = 0;
14
+ let timeoutCalls = 0;
15
+
16
+ @Injectable()
17
+ class TaskService {
18
+ @Cron('*/1 * * * * *') // Every second
19
+ handleCron() {
20
+ cronCalls++;
21
+ }
22
+
23
+ @Interval(100) // Every 100ms
24
+ handleInterval() {
25
+ intervalCalls++;
26
+ }
27
+
28
+ @Timeout(250) // After 250ms
29
+ handleTimeout() {
30
+ timeoutCalls++;
31
+ }
32
+ }
33
+
34
+ @Module({
35
+ imports: [ScheduleModule.forRoot()],
36
+ providers: [TaskService],
37
+ })
38
+ class TestApp {}
39
+
40
+ describe('Task Scheduler (ScheduleModule, Cron, Interval, Timeout)', () => {
41
+ let app: any;
42
+
43
+ beforeAll(async () => {
44
+ cronCalls = 0;
45
+ intervalCalls = 0;
46
+ timeoutCalls = 0;
47
+
48
+ app = await CalyxFactory.create(TestApp);
49
+ await app.init();
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await app.close();
54
+ });
55
+
56
+ test('should fire intervals, timeouts, and cron jobs on schedule', async () => {
57
+ // Wait 500ms
58
+ await new Promise((resolve) => setTimeout(resolve, 550));
59
+
60
+ expect(cronCalls).toBeGreaterThanOrEqual(0);
61
+ expect(intervalCalls).toBeGreaterThan(3);
62
+ expect(timeoutCalls).toBe(1);
63
+ });
64
+ });