@onebun/core 0.1.10 → 0.1.12
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.
- package/package.json +7 -6
- package/src/application/application.test.ts +92 -0
- package/src/application/application.ts +50 -13
- package/src/docs-examples.test.ts +9 -2
- package/src/index.ts +11 -1
- package/src/module/config.interface.ts +60 -0
- package/src/module/config.service.test.ts +56 -32
- package/src/module/config.service.ts +26 -9
- package/src/module/controller.test.ts +27 -29
- package/src/module/controller.ts +3 -2
- package/src/module/index.ts +1 -0
- package/src/module/module.test.ts +31 -32
- package/src/module/module.ts +9 -5
- package/src/module/service.test.ts +22 -20
- package/src/module/service.ts +5 -3
- package/src/queue/adapters/memory.adapter.test.ts +19 -3
- package/src/queue/adapters/redis.adapter.test.ts +289 -0
- package/src/queue/queue.service.test.ts +240 -0
- package/src/queue/scheduler.test.ts +22 -9
- package/src/redis/shared-redis.test.ts +255 -0
- package/src/testing/test-utils.ts +56 -0
- package/src/websocket/ws-client.test.ts +96 -48
- package/src/websocket/ws-integration.test.ts +21 -19
- package/src/websocket/ws-storage-redis.test.ts +517 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "Core package for OneBun framework - decorators, DI, modules, controllers",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -41,14 +41,15 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"effect": "^3.13.10",
|
|
43
43
|
"arktype": "^2.0.0",
|
|
44
|
-
"@onebun/logger": "^0.1.
|
|
45
|
-
"@onebun/envs": "^0.1.
|
|
46
|
-
"@onebun/metrics": "^0.1.
|
|
44
|
+
"@onebun/logger": "^0.1.5",
|
|
45
|
+
"@onebun/envs": "^0.1.4",
|
|
46
|
+
"@onebun/metrics": "^0.1.6",
|
|
47
47
|
"@onebun/requests": "^0.1.3",
|
|
48
|
-
"@onebun/trace": "^0.1.
|
|
48
|
+
"@onebun/trace": "^0.1.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"bun-types": "1.2.2"
|
|
51
|
+
"bun-types": "1.2.2",
|
|
52
|
+
"testcontainers": "^11.7.1"
|
|
52
53
|
},
|
|
53
54
|
"engines": {
|
|
54
55
|
"bun": "1.2.2"
|
|
@@ -164,6 +164,98 @@ describe('OneBunApplication', () => {
|
|
|
164
164
|
// The actual value access might need the config to be fully initialized
|
|
165
165
|
// which happens during runtime, not during construction
|
|
166
166
|
});
|
|
167
|
+
|
|
168
|
+
test('should provide typed access to config values via getConfig()', () => {
|
|
169
|
+
@Module({})
|
|
170
|
+
class TestModule {}
|
|
171
|
+
|
|
172
|
+
// Mock config that simulates typed IConfig<OneBunAppConfig>
|
|
173
|
+
const mockConfigValues = {
|
|
174
|
+
server: { port: 9991, host: 'localhost' },
|
|
175
|
+
};
|
|
176
|
+
const mockConfig = {
|
|
177
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
178
|
+
initialize: mock(async () => {}),
|
|
179
|
+
get: mock((path: string) => {
|
|
180
|
+
const parts = path.split('.');
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
182
|
+
let value: any = mockConfigValues;
|
|
183
|
+
for (const part of parts) {
|
|
184
|
+
value = value?.[part];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return value;
|
|
188
|
+
}),
|
|
189
|
+
values: mockConfigValues,
|
|
190
|
+
getSafeConfig: mock(() => mockConfigValues),
|
|
191
|
+
isInitialized: true,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const app = createTestApp(TestModule);
|
|
195
|
+
// Inject mock config
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
197
|
+
(app as any).config = mockConfig;
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
|
+
(app as any).configService = {
|
|
200
|
+
get: mockConfig.get, values: mockConfig.values, getSafeConfig: mockConfig.getSafeConfig, isInitialized: true,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// getConfig() returns IConfig<OneBunAppConfig> which provides typed .get() method
|
|
204
|
+
const config = app.getConfig();
|
|
205
|
+
expect(config).toBeDefined();
|
|
206
|
+
|
|
207
|
+
// Access values through the typed interface
|
|
208
|
+
// TypeScript will infer the correct types based on module augmentation
|
|
209
|
+
const port = config.get('server.port');
|
|
210
|
+
const host = config.get('server.host');
|
|
211
|
+
|
|
212
|
+
expect(port).toBe(9991);
|
|
213
|
+
expect(host).toBe('localhost');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('should provide typed access via getConfigValue() convenience method', () => {
|
|
217
|
+
@Module({})
|
|
218
|
+
class TestModule {}
|
|
219
|
+
|
|
220
|
+
// Mock config that simulates typed IConfig<OneBunAppConfig>
|
|
221
|
+
const mockConfigValues = {
|
|
222
|
+
app: { name: 'test-app', debug: true },
|
|
223
|
+
};
|
|
224
|
+
const mockConfig = {
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
226
|
+
initialize: mock(async () => {}),
|
|
227
|
+
get: mock((path: string) => {
|
|
228
|
+
const parts = path.split('.');
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
|
+
let value: any = mockConfigValues;
|
|
231
|
+
for (const part of parts) {
|
|
232
|
+
value = value?.[part];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return value;
|
|
236
|
+
}),
|
|
237
|
+
values: mockConfigValues,
|
|
238
|
+
getSafeConfig: mock(() => mockConfigValues),
|
|
239
|
+
isInitialized: true,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const app = createTestApp(TestModule);
|
|
243
|
+
// Inject mock config
|
|
244
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
245
|
+
(app as any).config = mockConfig;
|
|
246
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
247
|
+
(app as any).configService = {
|
|
248
|
+
get: mockConfig.get, values: mockConfig.values, getSafeConfig: mockConfig.getSafeConfig, isInitialized: true,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// getConfigValue() is a convenience method that delegates to getConfig().get()
|
|
252
|
+
// It also provides typed access based on OneBunAppConfig module augmentation
|
|
253
|
+
const appName = app.getConfigValue('app.name');
|
|
254
|
+
const debug = app.getConfigValue('app.debug');
|
|
255
|
+
|
|
256
|
+
expect(appName).toBe('test-app');
|
|
257
|
+
expect(debug).toBe(true);
|
|
258
|
+
});
|
|
167
259
|
});
|
|
168
260
|
|
|
169
261
|
describe('Layer methods', () => {
|
|
@@ -3,7 +3,11 @@ import { Effect, type Layer } from 'effect';
|
|
|
3
3
|
import type { Controller } from '../module/controller';
|
|
4
4
|
import type { WsClientData } from '../websocket/ws.types';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type DeepPaths,
|
|
8
|
+
type DeepValue,
|
|
9
|
+
TypedEnv,
|
|
10
|
+
} from '@onebun/envs';
|
|
7
11
|
import {
|
|
8
12
|
createSyncLogger,
|
|
9
13
|
type Logger,
|
|
@@ -21,6 +25,11 @@ import {
|
|
|
21
25
|
import { makeTraceService, TraceService } from '@onebun/trace';
|
|
22
26
|
|
|
23
27
|
import { getControllerMetadata } from '../decorators/decorators';
|
|
28
|
+
import {
|
|
29
|
+
NotInitializedConfig,
|
|
30
|
+
type IConfig,
|
|
31
|
+
type OneBunAppConfig,
|
|
32
|
+
} from '../module/config.interface';
|
|
24
33
|
import { ConfigServiceImpl } from '../module/config.service';
|
|
25
34
|
import { OneBunModule } from '../module/module';
|
|
26
35
|
import { QueueService, type QueueAdapter } from '../queue';
|
|
@@ -110,8 +119,7 @@ export class OneBunApplication {
|
|
|
110
119
|
development: process.env.NODE_ENV !== 'production',
|
|
111
120
|
};
|
|
112
121
|
private logger: SyncLogger;
|
|
113
|
-
|
|
114
|
-
private config: any = null;
|
|
122
|
+
private config: IConfig<OneBunAppConfig>;
|
|
115
123
|
private configService: ConfigServiceImpl | null = null;
|
|
116
124
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
125
|
private metricsService: any = null;
|
|
@@ -135,10 +143,12 @@ export class OneBunApplication {
|
|
|
135
143
|
this.options = { ...this.options, ...options };
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
// Initialize configuration if schema
|
|
146
|
+
// Initialize configuration - TypedEnv if schema provided, otherwise NotInitializedConfig
|
|
139
147
|
if (this.options.envSchema) {
|
|
140
148
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
141
149
|
this.config = TypedEnv.create(this.options.envSchema as any, this.options.envOptions);
|
|
150
|
+
} else {
|
|
151
|
+
this.config = new NotInitializedConfig();
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
// Use provided logger layer or create a default one
|
|
@@ -155,8 +165,8 @@ export class OneBunApplication {
|
|
|
155
165
|
) as Logger;
|
|
156
166
|
this.logger = createSyncLogger(effectLogger);
|
|
157
167
|
|
|
158
|
-
// Create configuration service if config is
|
|
159
|
-
if (this.config) {
|
|
168
|
+
// Create configuration service if config is initialized
|
|
169
|
+
if (this.config.isInitialized || !(this.config instanceof NotInitializedConfig)) {
|
|
160
170
|
this.configService = new ConfigServiceImpl(this.logger, this.config);
|
|
161
171
|
}
|
|
162
172
|
|
|
@@ -224,9 +234,21 @@ export class OneBunApplication {
|
|
|
224
234
|
}
|
|
225
235
|
|
|
226
236
|
/**
|
|
227
|
-
* Get configuration service
|
|
237
|
+
* Get configuration service with full type inference.
|
|
238
|
+
* Uses module augmentation of OneBunAppConfig for type-safe access.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* // With module augmentation:
|
|
242
|
+
* declare module '@onebun/core' {
|
|
243
|
+
* interface OneBunAppConfig {
|
|
244
|
+
* server: { port: number; host: string };
|
|
245
|
+
* }
|
|
246
|
+
* }
|
|
247
|
+
*
|
|
248
|
+
* const config = app.getConfig();
|
|
249
|
+
* const port = config.get('server.port'); // number
|
|
228
250
|
*/
|
|
229
|
-
getConfig():
|
|
251
|
+
getConfig(): IConfig<OneBunAppConfig> {
|
|
230
252
|
if (!this.configService) {
|
|
231
253
|
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
232
254
|
}
|
|
@@ -235,10 +257,25 @@ export class OneBunApplication {
|
|
|
235
257
|
}
|
|
236
258
|
|
|
237
259
|
/**
|
|
238
|
-
* Get configuration value by path (convenience method)
|
|
260
|
+
* Get configuration value by path (convenience method) with full type inference.
|
|
261
|
+
* Uses module augmentation of OneBunAppConfig for type-safe access.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* // With module augmentation:
|
|
265
|
+
* declare module '@onebun/core' {
|
|
266
|
+
* interface OneBunAppConfig {
|
|
267
|
+
* server: { port: number; host: string };
|
|
268
|
+
* }
|
|
269
|
+
* }
|
|
270
|
+
*
|
|
271
|
+
* const port = app.getConfigValue('server.port'); // number
|
|
272
|
+
* const host = app.getConfigValue('server.host'); // string
|
|
239
273
|
*/
|
|
240
|
-
getConfigValue<
|
|
241
|
-
|
|
274
|
+
getConfigValue<P extends DeepPaths<OneBunAppConfig>>(path: P): DeepValue<OneBunAppConfig, P>;
|
|
275
|
+
/** Fallback for dynamic paths */
|
|
276
|
+
getConfigValue<T = unknown>(path: string): T;
|
|
277
|
+
getConfigValue(path: string): unknown {
|
|
278
|
+
return this.getConfig().get(path);
|
|
242
279
|
}
|
|
243
280
|
|
|
244
281
|
/**
|
|
@@ -289,8 +326,8 @@ export class OneBunApplication {
|
|
|
289
326
|
*/
|
|
290
327
|
async start(): Promise<void> {
|
|
291
328
|
try {
|
|
292
|
-
// Initialize configuration if provided
|
|
293
|
-
if (this.config) {
|
|
329
|
+
// Initialize configuration if schema was provided
|
|
330
|
+
if (!(this.config instanceof NotInitializedConfig)) {
|
|
294
331
|
await this.config.initialize();
|
|
295
332
|
this.logger.info('Application configuration initialized');
|
|
296
333
|
}
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
createWsServiceDefinition,
|
|
81
81
|
createWsClient,
|
|
82
82
|
matchPattern,
|
|
83
|
+
makeMockLoggerLayer,
|
|
83
84
|
} from './';
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -157,6 +158,7 @@ describe('Minimal Working Example (docs/index.md)', () => {
|
|
|
157
158
|
const app = new OneBunApplication(AppModule, {
|
|
158
159
|
port: 3000,
|
|
159
160
|
envSchema,
|
|
161
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
160
162
|
metrics: { enabled: true },
|
|
161
163
|
tracing: { enabled: true },
|
|
162
164
|
});
|
|
@@ -1302,6 +1304,7 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
1302
1304
|
|
|
1303
1305
|
// From docs: OneBunApplication constructor
|
|
1304
1306
|
const app = new OneBunApplication(AppModule, {
|
|
1307
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
1305
1308
|
port: 3000,
|
|
1306
1309
|
basePath: '/api/v1',
|
|
1307
1310
|
});
|
|
@@ -1330,6 +1333,7 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
1330
1333
|
basePath: '/api/v1',
|
|
1331
1334
|
routePrefix: 'myservice',
|
|
1332
1335
|
development: true,
|
|
1336
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
1333
1337
|
metrics: {
|
|
1334
1338
|
enabled: true,
|
|
1335
1339
|
path: '/metrics',
|
|
@@ -1369,7 +1373,7 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
1369
1373
|
httpDurationBuckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
|
|
1370
1374
|
};
|
|
1371
1375
|
|
|
1372
|
-
const app = new OneBunApplication(AppModule, { metrics: metricsOptions });
|
|
1376
|
+
const app = new OneBunApplication(AppModule, { metrics: metricsOptions, loggerLayer: makeMockLoggerLayer() });
|
|
1373
1377
|
expect(app).toBeDefined();
|
|
1374
1378
|
});
|
|
1375
1379
|
|
|
@@ -1400,7 +1404,7 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
1400
1404
|
};
|
|
1401
1405
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
1402
1406
|
|
|
1403
|
-
const app = new OneBunApplication(AppModule, { tracing: tracingOptions });
|
|
1407
|
+
const app = new OneBunApplication(AppModule, { tracing: tracingOptions, loggerLayer: makeMockLoggerLayer() });
|
|
1404
1408
|
expect(app).toBeDefined();
|
|
1405
1409
|
});
|
|
1406
1410
|
});
|
|
@@ -2182,6 +2186,7 @@ describe('Getting Started Documentation (docs/getting-started.md)', () => {
|
|
|
2182
2186
|
loadDotEnv: true,
|
|
2183
2187
|
envFilePath: '.env',
|
|
2184
2188
|
},
|
|
2189
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
2185
2190
|
metrics: {
|
|
2186
2191
|
enabled: true,
|
|
2187
2192
|
path: '/metrics',
|
|
@@ -2589,6 +2594,7 @@ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
|
2589
2594
|
pingTimeout: 20000,
|
|
2590
2595
|
maxPayload: 1048576,
|
|
2591
2596
|
},
|
|
2597
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
2592
2598
|
});
|
|
2593
2599
|
|
|
2594
2600
|
expect(app).toBeDefined();
|
|
@@ -2865,6 +2871,7 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
|
|
|
2865
2871
|
pingInterval: 25000,
|
|
2866
2872
|
pingTimeout: 20000,
|
|
2867
2873
|
},
|
|
2874
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
2868
2875
|
});
|
|
2869
2876
|
|
|
2870
2877
|
expect(app).toBeDefined();
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// Re-export from external packages
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
Env,
|
|
4
|
+
type EnvSchema,
|
|
5
|
+
EnvValidationError,
|
|
6
|
+
type InferConfigType,
|
|
7
|
+
type EnvVariableConfig,
|
|
8
|
+
} from '@onebun/envs';
|
|
3
9
|
export type { SyncLogger } from '@onebun/logger';
|
|
4
10
|
export {
|
|
5
11
|
createHttpClient,
|
|
@@ -56,6 +62,10 @@ export {
|
|
|
56
62
|
// Global modules support
|
|
57
63
|
clearGlobalServicesRegistry,
|
|
58
64
|
getGlobalServicesRegistry,
|
|
65
|
+
// Config interface types
|
|
66
|
+
type IConfig,
|
|
67
|
+
type OneBunAppConfig,
|
|
68
|
+
NotInitializedConfig,
|
|
59
69
|
} from './module';
|
|
60
70
|
|
|
61
71
|
// Application
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DeepPaths, DeepValue } from '@onebun/envs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Empty interface for module augmentation.
|
|
5
|
+
* Users extend this to provide typed config access.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* declare module '@onebun/core' {
|
|
9
|
+
* interface OneBunAppConfig {
|
|
10
|
+
* server: { port: number; host: string };
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface OneBunAppConfig {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generic typed configuration interface.
|
|
19
|
+
* Provides type-safe access to configuration values.
|
|
20
|
+
*/
|
|
21
|
+
export interface IConfig<T = OneBunAppConfig> {
|
|
22
|
+
/** Get configuration value by dot-notation path with full type inference */
|
|
23
|
+
get<P extends DeepPaths<T>>(path: P): DeepValue<T, P>;
|
|
24
|
+
/** Fallback for dynamic paths */
|
|
25
|
+
get(path: string): unknown;
|
|
26
|
+
/** Get all configuration values */
|
|
27
|
+
readonly values: T;
|
|
28
|
+
/** Get safe configuration for logging (sensitive data masked) */
|
|
29
|
+
getSafeConfig(): T;
|
|
30
|
+
/** Check if configuration is initialized */
|
|
31
|
+
readonly isInitialized: boolean;
|
|
32
|
+
/** Initialize configuration (async) */
|
|
33
|
+
initialize(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Config stub that throws on any access.
|
|
38
|
+
* Used when envSchema is not provided.
|
|
39
|
+
*/
|
|
40
|
+
export class NotInitializedConfig implements IConfig<never> {
|
|
41
|
+
get(_path: string): never {
|
|
42
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get values(): never {
|
|
46
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getSafeConfig(): never {
|
|
50
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get isInitialized(): boolean {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async initialize(): Promise<void> {
|
|
58
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
mock,
|
|
12
12
|
} from 'bun:test';
|
|
13
13
|
|
|
14
|
+
import type { IConfig, OneBunAppConfig } from './config.interface';
|
|
15
|
+
|
|
14
16
|
import type { SyncLogger } from '@onebun/logger';
|
|
15
17
|
|
|
16
18
|
import {
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
describe('ConfigService', () => {
|
|
23
25
|
let mockLogger: SyncLogger;
|
|
24
26
|
|
|
25
|
-
let mockConfig:
|
|
27
|
+
let mockConfig: IConfig<OneBunAppConfig>;
|
|
26
28
|
|
|
27
29
|
beforeEach(() => {
|
|
28
30
|
mockLogger = {
|
|
@@ -38,8 +40,8 @@ describe('ConfigService', () => {
|
|
|
38
40
|
mockConfig = {
|
|
39
41
|
initialize: mock(async () => {}),
|
|
40
42
|
get: mock((path: string): any => `value-for-${path}`),
|
|
41
|
-
values: { test: 'value' },
|
|
42
|
-
getSafeConfig: mock(() => ({ test: '***' })),
|
|
43
|
+
values: { test: 'value' } as unknown as OneBunAppConfig,
|
|
44
|
+
getSafeConfig: mock(() => ({ test: '***' })) as unknown as () => OneBunAppConfig,
|
|
43
45
|
isInitialized: true,
|
|
44
46
|
};
|
|
45
47
|
});
|
|
@@ -63,7 +65,7 @@ describe('ConfigService', () => {
|
|
|
63
65
|
const service = new ConfigServiceImpl(mockLogger);
|
|
64
66
|
|
|
65
67
|
expect(service).toBeInstanceOf(ConfigServiceImpl);
|
|
66
|
-
expect(service.instance).
|
|
68
|
+
expect(service.instance).toBeNull();
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
test('should create instance with config but without logger (uninitialized)', () => {
|
|
@@ -83,8 +85,8 @@ describe('ConfigService', () => {
|
|
|
83
85
|
expect(mockLogger.info).toHaveBeenCalledWith('Configuration initialized successfully');
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
test('should not throw when config instance is
|
|
87
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
88
|
+
test('should not throw when config instance is undefined', async () => {
|
|
89
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
88
90
|
|
|
89
91
|
await expect(service.initialize()).resolves.toBeUndefined();
|
|
90
92
|
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
@@ -109,7 +111,7 @@ describe('ConfigService', () => {
|
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
test('should throw error when config not initialized', () => {
|
|
112
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
114
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
113
115
|
|
|
114
116
|
expect(() => service.get('test.path')).toThrow(
|
|
115
117
|
'Configuration not initialized. Provide envSchema in ApplicationOptions.',
|
|
@@ -117,8 +119,14 @@ describe('ConfigService', () => {
|
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
test('should return typed value', () => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
const typedConfig = {
|
|
123
|
+
initialize: mock(async () => {}),
|
|
124
|
+
get: mock((path: string) => path === 'test.number' ? 42 : undefined),
|
|
125
|
+
values: { test: 'value' } as unknown as OneBunAppConfig,
|
|
126
|
+
getSafeConfig: mock(() => ({ test: '***' })) as unknown as () => OneBunAppConfig,
|
|
127
|
+
isInitialized: true,
|
|
128
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
129
|
+
const service = new ConfigServiceImpl(mockLogger, typedConfig);
|
|
122
130
|
|
|
123
131
|
const result = service.get<number>('test.number');
|
|
124
132
|
|
|
@@ -133,11 +141,12 @@ describe('ConfigService', () => {
|
|
|
133
141
|
|
|
134
142
|
const result = service.values;
|
|
135
143
|
|
|
136
|
-
|
|
144
|
+
// Use unknown cast because tests use mock data that doesn't match OneBunAppConfig augmentation
|
|
145
|
+
expect(result as unknown).toEqual({ test: 'value' });
|
|
137
146
|
});
|
|
138
147
|
|
|
139
148
|
test('should throw error when config not initialized', () => {
|
|
140
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
149
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
141
150
|
|
|
142
151
|
expect(() => service.values).toThrow(
|
|
143
152
|
'Configuration not initialized. Provide envSchema in ApplicationOptions.',
|
|
@@ -151,12 +160,13 @@ describe('ConfigService', () => {
|
|
|
151
160
|
|
|
152
161
|
const result = service.getSafeConfig();
|
|
153
162
|
|
|
154
|
-
|
|
163
|
+
// Use unknown cast because tests use mock data that doesn't match OneBunAppConfig augmentation
|
|
164
|
+
expect(result as unknown).toEqual({ test: '***' });
|
|
155
165
|
expect(mockConfig.getSafeConfig).toHaveBeenCalled();
|
|
156
166
|
});
|
|
157
167
|
|
|
158
168
|
test('should throw error when config not initialized', () => {
|
|
159
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
169
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
160
170
|
|
|
161
171
|
expect(() => service.getSafeConfig()).toThrow(
|
|
162
172
|
'Configuration not initialized. Provide envSchema in ApplicationOptions.',
|
|
@@ -172,14 +182,20 @@ describe('ConfigService', () => {
|
|
|
172
182
|
});
|
|
173
183
|
|
|
174
184
|
test('should return false when config not initialized', () => {
|
|
175
|
-
|
|
176
|
-
|
|
185
|
+
const uninitConfig = {
|
|
186
|
+
initialize: mock(async () => {}),
|
|
187
|
+
get: mock((path: string): any => `value-for-${path}`),
|
|
188
|
+
values: { test: 'value' } as unknown as OneBunAppConfig,
|
|
189
|
+
getSafeConfig: mock(() => ({ test: '***' })) as unknown as () => OneBunAppConfig,
|
|
190
|
+
isInitialized: false,
|
|
191
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
192
|
+
const service = new ConfigServiceImpl(mockLogger, uninitConfig);
|
|
177
193
|
|
|
178
194
|
expect(service.isInitialized).toBe(false);
|
|
179
195
|
});
|
|
180
196
|
|
|
181
|
-
test('should return false when config instance is
|
|
182
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
197
|
+
test('should return false when config instance is undefined', () => {
|
|
198
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
183
199
|
|
|
184
200
|
expect(service.isInitialized).toBe(false);
|
|
185
201
|
});
|
|
@@ -193,7 +209,7 @@ describe('ConfigService', () => {
|
|
|
193
209
|
});
|
|
194
210
|
|
|
195
211
|
test('should return null when no config provided', () => {
|
|
196
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
212
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
197
213
|
|
|
198
214
|
expect(service.instance).toBeNull();
|
|
199
215
|
});
|
|
@@ -210,7 +226,7 @@ describe('ConfigService', () => {
|
|
|
210
226
|
test('should handle undefined config in constructor', () => {
|
|
211
227
|
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
212
228
|
|
|
213
|
-
expect(service.instance).
|
|
229
|
+
expect(service.instance).toBeNull();
|
|
214
230
|
expect(service.isInitialized).toBe(false);
|
|
215
231
|
});
|
|
216
232
|
});
|
|
@@ -219,8 +235,11 @@ describe('ConfigService', () => {
|
|
|
219
235
|
test('should handle initialization error gracefully', async () => {
|
|
220
236
|
const errorConfig = {
|
|
221
237
|
initialize: mock(() => Promise.reject(new Error('Init failed'))),
|
|
238
|
+
get: mock(() => undefined),
|
|
239
|
+
values: {} as unknown as OneBunAppConfig,
|
|
240
|
+
getSafeConfig: mock(() => ({})) as unknown as () => OneBunAppConfig,
|
|
222
241
|
isInitialized: false,
|
|
223
|
-
}
|
|
242
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
224
243
|
|
|
225
244
|
const service = new ConfigServiceImpl(mockLogger, errorConfig);
|
|
226
245
|
|
|
@@ -229,15 +248,16 @@ describe('ConfigService', () => {
|
|
|
229
248
|
|
|
230
249
|
test('should handle config method errors', () => {
|
|
231
250
|
const errorConfig = {
|
|
251
|
+
initialize: mock(async () => {}),
|
|
232
252
|
get: mock(() => {
|
|
233
253
|
throw new Error('Get failed');
|
|
234
254
|
}),
|
|
235
|
-
values: null,
|
|
255
|
+
values: null as unknown as unknown as OneBunAppConfig,
|
|
236
256
|
getSafeConfig: mock(() => {
|
|
237
257
|
throw new Error('Safe config failed');
|
|
238
|
-
}),
|
|
258
|
+
}) as unknown as () => OneBunAppConfig,
|
|
239
259
|
isInitialized: true,
|
|
240
|
-
}
|
|
260
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
241
261
|
|
|
242
262
|
const service = new ConfigServiceImpl(mockLogger, errorConfig);
|
|
243
263
|
|
|
@@ -249,11 +269,12 @@ describe('ConfigService', () => {
|
|
|
249
269
|
describe('edge cases', () => {
|
|
250
270
|
test('should handle config with null values', () => {
|
|
251
271
|
const nullConfig = {
|
|
272
|
+
initialize: mock(async () => {}),
|
|
252
273
|
get: mock(() => null),
|
|
253
|
-
values: null,
|
|
254
|
-
getSafeConfig: mock(() => null),
|
|
274
|
+
values: null as unknown as unknown as OneBunAppConfig,
|
|
275
|
+
getSafeConfig: mock(() => null as unknown as unknown as OneBunAppConfig),
|
|
255
276
|
isInitialized: true,
|
|
256
|
-
}
|
|
277
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
257
278
|
|
|
258
279
|
const service = new ConfigServiceImpl(mockLogger, nullConfig);
|
|
259
280
|
|
|
@@ -264,11 +285,12 @@ describe('ConfigService', () => {
|
|
|
264
285
|
|
|
265
286
|
test('should handle config with undefined values', () => {
|
|
266
287
|
const undefinedConfig = {
|
|
288
|
+
initialize: mock(async () => {}),
|
|
267
289
|
get: mock(() => undefined),
|
|
268
|
-
values: undefined,
|
|
269
|
-
getSafeConfig: mock(() => undefined),
|
|
290
|
+
values: undefined as unknown as unknown as OneBunAppConfig,
|
|
291
|
+
getSafeConfig: mock(() => undefined as unknown as unknown as OneBunAppConfig),
|
|
270
292
|
isInitialized: false,
|
|
271
|
-
}
|
|
293
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
272
294
|
|
|
273
295
|
const service = new ConfigServiceImpl(mockLogger, undefinedConfig);
|
|
274
296
|
|
|
@@ -318,19 +340,21 @@ describe('ConfigService', () => {
|
|
|
318
340
|
api: { key: '***', timeout: 30000 },
|
|
319
341
|
})),
|
|
320
342
|
isInitialized: true,
|
|
321
|
-
}
|
|
343
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
322
344
|
|
|
323
345
|
const service = new ConfigServiceImpl(mockLogger, complexConfig);
|
|
324
346
|
|
|
325
347
|
expect((service as any).get('database.host')).toBe('localhost');
|
|
326
348
|
expect((service as any).get('database.port')).toBe(5432);
|
|
327
|
-
|
|
349
|
+
// Use unknown cast because tests use mock data that doesn't match OneBunAppConfig augmentation
|
|
350
|
+
expect(service.values as unknown).toEqual({
|
|
328
351
|
database: { host: 'localhost', port: 5432 },
|
|
329
352
|
api: { key: 'secret', timeout: 30000 },
|
|
330
353
|
});
|
|
331
354
|
|
|
332
355
|
const safeConfig = service.getSafeConfig();
|
|
333
|
-
|
|
356
|
+
// Use unknown cast because tests use mock data that doesn't match OneBunAppConfig augmentation
|
|
357
|
+
expect(safeConfig as unknown).toEqual({
|
|
334
358
|
database: { host: 'localhost', port: 5432 },
|
|
335
359
|
api: { key: '***', timeout: 30000 },
|
|
336
360
|
});
|