@flusys/nestjs-core 6.0.2 → 6.1.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.
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", {
3
+ value: true
4
+ });
5
+ const _datasourceconfigbuilder = require("./datasource-config.builder");
6
+ describe('datasource-config.builder', ()=>{
7
+ describe('buildDataSourceOptions', ()=>{
8
+ it('should build options with synchronize disabled', ()=>{
9
+ const config = {
10
+ type: 'postgres',
11
+ host: 'localhost',
12
+ port: 5432,
13
+ username: 'root',
14
+ password: 'pw',
15
+ database: 'flusys'
16
+ };
17
+ const options = (0, _datasourceconfigbuilder.buildDataSourceOptions)(config, [
18
+ Object
19
+ ], 'migrations/default', 'custom_migrations');
20
+ expect(options).toEqual({
21
+ type: 'postgres',
22
+ host: 'localhost',
23
+ port: 5432,
24
+ username: 'root',
25
+ password: 'pw',
26
+ database: 'flusys',
27
+ entities: [
28
+ Object
29
+ ],
30
+ migrations: [
31
+ 'migrations/default'
32
+ ],
33
+ migrationsTableName: 'custom_migrations',
34
+ synchronize: false
35
+ });
36
+ });
37
+ it('should default database to an empty string and table name to "migrations"', ()=>{
38
+ const config = {
39
+ type: 'sqlite'
40
+ };
41
+ const options = (0, _datasourceconfigbuilder.buildDataSourceOptions)(config, [], 'migrations/default');
42
+ expect(options.database).toBe('');
43
+ expect(options.migrationsTableName).toBe('migrations');
44
+ });
45
+ });
46
+ describe('getMigrationsFolderPath', ()=>{
47
+ it('should use "default" when no tenantId is given', ()=>{
48
+ expect((0, _datasourceconfigbuilder.getMigrationsFolderPath)('migrations')).toBe('migrations/default');
49
+ });
50
+ it('should namespace the folder by tenant', ()=>{
51
+ expect((0, _datasourceconfigbuilder.getMigrationsFolderPath)('migrations', 'tenant-a')).toBe('migrations/tenant-a');
52
+ });
53
+ });
54
+ describe('getMigrationsGlobPattern', ()=>{
55
+ it('should build a glob matching ts and js migration files', ()=>{
56
+ expect((0, _datasourceconfigbuilder.getMigrationsGlobPattern)('migrations', 'tenant-a')).toBe('migrations/tenant-a/*{.ts,.js}');
57
+ });
58
+ it('should fall back to the default folder', ()=>{
59
+ expect((0, _datasourceconfigbuilder.getMigrationsGlobPattern)('migrations')).toBe('migrations/default/*{.ts,.js}');
60
+ });
61
+ });
62
+ describe('getDatabaseForTenant', ()=>{
63
+ const baseConfig = {
64
+ defaultDatabaseConfig: {
65
+ type: 'postgres',
66
+ database: 'flusys_default'
67
+ },
68
+ migrationsPath: 'migrations',
69
+ entities: []
70
+ };
71
+ it('should return the default tenant config when tenantId is omitted', ()=>{
72
+ const result = (0, _datasourceconfigbuilder.getDatabaseForTenant)(baseConfig);
73
+ expect(result.database).toBe('flusys_default');
74
+ expect(result.tenant).toBeUndefined();
75
+ expect(result.tenantConfig).toEqual({
76
+ id: 'default',
77
+ database: 'flusys_default',
78
+ enableCompanyFeature: false,
79
+ permissionMode: 'FULL'
80
+ });
81
+ });
82
+ it('should apply bootstrap feature flags to the default tenant config', ()=>{
83
+ const config = {
84
+ ...baseConfig,
85
+ bootstrapAppConfig: {
86
+ databaseMode: 'single',
87
+ enableCompanyFeature: true,
88
+ permissionMode: 'RBAC'
89
+ }
90
+ };
91
+ const result = (0, _datasourceconfigbuilder.getDatabaseForTenant)(config);
92
+ expect(result.tenantConfig.enableCompanyFeature).toBe(true);
93
+ expect(result.tenantConfig.permissionMode).toBe('RBAC');
94
+ });
95
+ it('should resolve an explicit tenant by id', ()=>{
96
+ const config = {
97
+ ...baseConfig,
98
+ tenants: [
99
+ {
100
+ id: 'tenant-a',
101
+ database: 'db_a'
102
+ },
103
+ {
104
+ id: 'tenant-b',
105
+ database: 'db_b'
106
+ }
107
+ ]
108
+ };
109
+ const result = (0, _datasourceconfigbuilder.getDatabaseForTenant)(config, 'tenant-b');
110
+ expect(result.database).toBe('db_b');
111
+ expect(result.tenant?.id).toBe('tenant-b');
112
+ expect(result.tenantConfig.id).toBe('tenant-b');
113
+ });
114
+ it('should throw a descriptive error for an unknown tenant id', ()=>{
115
+ const config = {
116
+ ...baseConfig,
117
+ tenants: [
118
+ {
119
+ id: 'tenant-a',
120
+ database: 'db_a'
121
+ }
122
+ ]
123
+ };
124
+ expect(()=>(0, _datasourceconfigbuilder.getDatabaseForTenant)(config, 'unknown')).toThrow("Tenant 'unknown' not found. Available: tenant-a");
125
+ });
126
+ });
127
+ describe('resolveDatabaseConnection', ()=>{
128
+ const baseConfig = {
129
+ defaultDatabaseConfig: {
130
+ type: 'postgres',
131
+ host: 'default-host',
132
+ port: 5432,
133
+ username: 'default-user',
134
+ password: 'default-pw',
135
+ database: 'flusys_default'
136
+ },
137
+ migrationsPath: 'migrations',
138
+ entities: []
139
+ };
140
+ it('should return the default connection when no tenant matches', ()=>{
141
+ const result = (0, _datasourceconfigbuilder.resolveDatabaseConnection)(baseConfig);
142
+ expect(result).toEqual(baseConfig.defaultDatabaseConfig);
143
+ });
144
+ it('should overlay tenant-specific fields onto the default connection', ()=>{
145
+ const config = {
146
+ ...baseConfig,
147
+ tenants: [
148
+ {
149
+ id: 'tenant-a',
150
+ database: 'db_a',
151
+ host: 'tenant-host'
152
+ }
153
+ ]
154
+ };
155
+ const result = (0, _datasourceconfigbuilder.resolveDatabaseConnection)(config, 'tenant-a');
156
+ expect(result).toEqual({
157
+ type: 'postgres',
158
+ host: 'tenant-host',
159
+ port: 5432,
160
+ username: 'default-user',
161
+ password: 'default-pw',
162
+ database: 'db_a'
163
+ });
164
+ });
165
+ it('should fall back to default connection fields when tenant omits them', ()=>{
166
+ const config = {
167
+ ...baseConfig,
168
+ tenants: [
169
+ {
170
+ id: 'tenant-a',
171
+ database: 'db_a'
172
+ }
173
+ ]
174
+ };
175
+ const result = (0, _datasourceconfigbuilder.resolveDatabaseConnection)(config, 'tenant-a');
176
+ expect(result.host).toBe('default-host');
177
+ expect(result.port).toBe(5432);
178
+ expect(result.username).toBe('default-user');
179
+ });
180
+ });
181
+ describe('resolveEntities', ()=>{
182
+ it('should return the entities array directly when not a function', ()=>{
183
+ const config = {
184
+ entities: [
185
+ Object,
186
+ String
187
+ ]
188
+ };
189
+ expect((0, _datasourceconfigbuilder.resolveEntities)(config)).toEqual([
190
+ Object,
191
+ String
192
+ ]);
193
+ });
194
+ it('should invoke the resolver function with tenant config', ()=>{
195
+ const resolver = jest.fn().mockReturnValue([
196
+ Number
197
+ ]);
198
+ const config = {
199
+ entities: resolver
200
+ };
201
+ const tenantConfig = {
202
+ id: 'tenant-a',
203
+ database: 'db_a'
204
+ };
205
+ const result = (0, _datasourceconfigbuilder.resolveEntities)(config, tenantConfig);
206
+ expect(resolver).toHaveBeenCalledWith(tenantConfig);
207
+ expect(result).toEqual([
208
+ Number
209
+ ]);
210
+ });
211
+ });
212
+ describe('getActiveTenants', ()=>{
213
+ it('should return an empty array when no tenants are configured', ()=>{
214
+ const config = {
215
+ tenants: undefined
216
+ };
217
+ expect((0, _datasourceconfigbuilder.getActiveTenants)(config)).toEqual([]);
218
+ });
219
+ it('should return the configured tenants', ()=>{
220
+ const tenants = [
221
+ {
222
+ id: 'a',
223
+ database: 'db_a'
224
+ }
225
+ ];
226
+ const config = {
227
+ tenants
228
+ };
229
+ expect((0, _datasourceconfigbuilder.getActiveTenants)(config)).toBe(tenants);
230
+ });
231
+ });
232
+ });
@@ -0,0 +1,321 @@
1
+ import { envConfig } from './env-config.service';
2
+ describe('EnvConfigService', ()=>{
3
+ const ORIGINAL_ENV = {
4
+ ...process.env
5
+ };
6
+ afterEach(()=>{
7
+ for (const key of Object.keys(process.env)){
8
+ if (!(key in ORIGINAL_ENV)) delete process.env[key];
9
+ }
10
+ Object.assign(process.env, ORIGINAL_ENV);
11
+ });
12
+ describe('tryGetValue', ()=>{
13
+ it('should return env value for an unrestricted key', ()=>{
14
+ process.env.CUSTOM_KEY = 'custom-value';
15
+ expect(envConfig.tryGetValue('CUSTOM_KEY')).toBe('custom-value');
16
+ });
17
+ it('should return empty string when missing and throwOnMissing is false', ()=>{
18
+ delete process.env.MISSING_KEY;
19
+ expect(envConfig.tryGetValue('MISSING_KEY', false)).toBe('');
20
+ });
21
+ it('should throw when missing and throwOnMissing is true', ()=>{
22
+ delete process.env.MISSING_KEY;
23
+ expect(()=>envConfig.tryGetValue('MISSING_KEY', true)).toThrow('Config error - missing env.MISSING_KEY');
24
+ });
25
+ it.each([
26
+ [
27
+ 'FRONTEND_URL',
28
+ 'getFrontendUrl'
29
+ ],
30
+ [
31
+ 'ALLOW_ORIGINS',
32
+ 'getOrigins'
33
+ ],
34
+ [
35
+ 'PORT',
36
+ 'getPort'
37
+ ],
38
+ [
39
+ 'MODE',
40
+ 'isProduction'
41
+ ],
42
+ [
43
+ 'DB_TYPE',
44
+ 'getTypeOrmConfig'
45
+ ],
46
+ [
47
+ 'DB_HOST',
48
+ 'getTypeOrmConfig'
49
+ ],
50
+ [
51
+ 'DB_PORT',
52
+ 'getTypeOrmConfig'
53
+ ],
54
+ [
55
+ 'DB_USER',
56
+ 'getTypeOrmConfig'
57
+ ],
58
+ [
59
+ 'DB_PASSWORD',
60
+ 'getTypeOrmConfig'
61
+ ],
62
+ [
63
+ 'DB_NAME',
64
+ 'getTypeOrmConfig'
65
+ ]
66
+ ])('should deny direct access to restricted key %s and point to %s()', (key, method)=>{
67
+ expect(()=>envConfig.tryGetValue(key)).toThrow(`Access denied for env key "${key}". Use the method \`${method}()\` instead.`);
68
+ });
69
+ });
70
+ describe('getNumber', ()=>{
71
+ it('should coerce a numeric env value', ()=>{
72
+ process.env.SOME_NUMBER = '42';
73
+ expect(envConfig.getNumber('SOME_NUMBER')).toBe(42);
74
+ });
75
+ it('should return NaN for a non-numeric value', ()=>{
76
+ process.env.SOME_NUMBER = 'not-a-number';
77
+ expect(envConfig.getNumber('SOME_NUMBER')).toBeNaN();
78
+ });
79
+ });
80
+ describe('getBoolean', ()=>{
81
+ it.each([
82
+ [
83
+ 'true',
84
+ true
85
+ ],
86
+ [
87
+ 'TRUE',
88
+ true
89
+ ],
90
+ [
91
+ 'True',
92
+ true
93
+ ],
94
+ [
95
+ 'false',
96
+ false
97
+ ],
98
+ [
99
+ 'yes',
100
+ false
101
+ ],
102
+ [
103
+ '',
104
+ false
105
+ ]
106
+ ])('should parse %s as %s', (value, expected)=>{
107
+ process.env.SOME_BOOL = value;
108
+ expect(envConfig.getBoolean('SOME_BOOL', false)).toBe(expected);
109
+ });
110
+ });
111
+ describe('getPort', ()=>{
112
+ it('should default to 3000 when PORT is unset', ()=>{
113
+ delete process.env.PORT;
114
+ expect(envConfig.getPort()).toBe(3000);
115
+ });
116
+ it('should return the configured PORT', ()=>{
117
+ process.env.PORT = '2002';
118
+ expect(envConfig.getPort()).toBe(2002);
119
+ });
120
+ });
121
+ describe('isProduction', ()=>{
122
+ it('should be true when MODE is unset', ()=>{
123
+ delete process.env.MODE;
124
+ expect(envConfig.isProduction()).toBe(true);
125
+ });
126
+ it('should be false when MODE is "dev" (case-insensitive)', ()=>{
127
+ process.env.MODE = 'dev';
128
+ expect(envConfig.isProduction()).toBe(false);
129
+ process.env.MODE = 'DEV';
130
+ expect(envConfig.isProduction()).toBe(false);
131
+ });
132
+ it('should be true for any non-DEV mode', ()=>{
133
+ process.env.MODE = 'production';
134
+ expect(envConfig.isProduction()).toBe(true);
135
+ });
136
+ });
137
+ describe('getOrigins', ()=>{
138
+ it('should split and trim a comma-separated origin list', ()=>{
139
+ process.env.ALLOW_ORIGINS = 'http://localhost:2001, http://localhost:3000 ,http://example.com';
140
+ expect(envConfig.getOrigins()).toEqual([
141
+ 'http://localhost:2001',
142
+ 'http://localhost:3000',
143
+ 'http://example.com'
144
+ ]);
145
+ });
146
+ it('should throw when unset since getOrigins requires the value', ()=>{
147
+ delete process.env.ALLOW_ORIGINS;
148
+ expect(()=>envConfig.getOrigins()).toThrow('Config error - missing env.ALLOW_ORIGINS');
149
+ });
150
+ });
151
+ describe('getTypeOrmConfig', ()=>{
152
+ it('should apply mysql defaults when DB_TYPE is unset', ()=>{
153
+ delete process.env.DB_TYPE;
154
+ delete process.env.DB_HOST;
155
+ delete process.env.DB_PORT;
156
+ delete process.env.DB_USER;
157
+ delete process.env.DB_PASSWORD;
158
+ delete process.env.DB_NAME;
159
+ expect(envConfig.getTypeOrmConfig()).toEqual({
160
+ type: 'mysql',
161
+ host: 'localhost',
162
+ port: 3306,
163
+ username: 'root',
164
+ password: '',
165
+ database: 'flusys_main'
166
+ });
167
+ });
168
+ it('should default the postgres port to 5432', ()=>{
169
+ process.env.DB_TYPE = 'postgres';
170
+ delete process.env.DB_PORT;
171
+ expect(envConfig.getTypeOrmConfig().port).toBe(5432);
172
+ });
173
+ it('should use explicit connection values when provided', ()=>{
174
+ process.env.DB_TYPE = 'postgres';
175
+ process.env.DB_HOST = 'db.internal';
176
+ process.env.DB_PORT = '5555';
177
+ process.env.DB_USER = 'flusys';
178
+ process.env.DB_PASSWORD = 'secret';
179
+ process.env.DB_NAME = 'flusys_prod';
180
+ expect(envConfig.getTypeOrmConfig()).toEqual({
181
+ type: 'postgres',
182
+ host: 'db.internal',
183
+ port: 5555,
184
+ username: 'flusys',
185
+ password: 'secret',
186
+ database: 'flusys_prod'
187
+ });
188
+ });
189
+ });
190
+ describe('getJwtConfig', ()=>{
191
+ it('should throw when JWT_SECRET is missing', ()=>{
192
+ delete process.env.JWT_SECRET;
193
+ process.env.REFRESH_TOKEN_SECRET = 'refresh';
194
+ expect(()=>envConfig.getJwtConfig()).toThrow('Config error - missing env.JWT_SECRET');
195
+ });
196
+ it('should throw when REFRESH_TOKEN_SECRET is missing', ()=>{
197
+ process.env.JWT_SECRET = 'secret';
198
+ delete process.env.REFRESH_TOKEN_SECRET;
199
+ expect(()=>envConfig.getJwtConfig()).toThrow('Config error - missing env.REFRESH_TOKEN_SECRET');
200
+ });
201
+ it('should apply default expirations when unset', ()=>{
202
+ process.env.JWT_SECRET = 'secret';
203
+ process.env.REFRESH_TOKEN_SECRET = 'refresh-secret';
204
+ delete process.env.JWT_EXPIRATION;
205
+ delete process.env.REFRESH_TOKEN_EXPIRATION;
206
+ expect(envConfig.getJwtConfig()).toEqual({
207
+ secret: 'secret',
208
+ expiration: '1h',
209
+ refreshSecret: 'refresh-secret',
210
+ refreshExpiration: '7d'
211
+ });
212
+ });
213
+ it('should use explicit expirations when set', ()=>{
214
+ process.env.JWT_SECRET = 'secret';
215
+ process.env.JWT_EXPIRATION = '15m';
216
+ process.env.REFRESH_TOKEN_SECRET = 'refresh-secret';
217
+ process.env.REFRESH_TOKEN_EXPIRATION = '30d';
218
+ expect(envConfig.getJwtConfig()).toEqual({
219
+ secret: 'secret',
220
+ expiration: '15m',
221
+ refreshSecret: 'refresh-secret',
222
+ refreshExpiration: '30d'
223
+ });
224
+ });
225
+ });
226
+ describe('getRedisUrl', ()=>{
227
+ it('should default to localhost redis', ()=>{
228
+ delete process.env.REDIS_URL;
229
+ expect(envConfig.getRedisUrl()).toBe('redis://localhost:6379');
230
+ });
231
+ it('should return the configured redis url', ()=>{
232
+ process.env.REDIS_URL = 'redis://redis-host:6380';
233
+ expect(envConfig.getRedisUrl()).toBe('redis://redis-host:6380');
234
+ });
235
+ });
236
+ describe('getMailConfig', ()=>{
237
+ it('should default to empty strings when unset', ()=>{
238
+ delete process.env.MAIL_FROM;
239
+ delete process.env.MAIL_APP_PASSWORD;
240
+ expect(envConfig.getMailConfig()).toEqual({
241
+ MAIL_FROM: '',
242
+ MAIL_APP_PASSWORD: ''
243
+ });
244
+ });
245
+ it('should return configured mail settings', ()=>{
246
+ process.env.MAIL_FROM = 'noreply@flusys.io';
247
+ process.env.MAIL_APP_PASSWORD = 'app-pass';
248
+ expect(envConfig.getMailConfig()).toEqual({
249
+ MAIL_FROM: 'noreply@flusys.io',
250
+ MAIL_APP_PASSWORD: 'app-pass'
251
+ });
252
+ });
253
+ });
254
+ describe('getTenantId', ()=>{
255
+ it('should return null when unset', ()=>{
256
+ delete process.env.TENANT_ID;
257
+ expect(envConfig.getTenantId()).toBeNull();
258
+ });
259
+ it('should return the configured tenant id', ()=>{
260
+ process.env.TENANT_ID = 'tenant-a';
261
+ expect(envConfig.getTenantId()).toBe('tenant-a');
262
+ });
263
+ });
264
+ describe('useTenantMode', ()=>{
265
+ it('should be false by default', ()=>{
266
+ delete process.env.USE_TENANT_MODE;
267
+ expect(envConfig.useTenantMode()).toBe(false);
268
+ });
269
+ it('should be true when explicitly set', ()=>{
270
+ process.env.USE_TENANT_MODE = 'true';
271
+ expect(envConfig.useTenantMode()).toBe(true);
272
+ });
273
+ });
274
+ describe('getLogConfig', ()=>{
275
+ it('should default log level to info in production', ()=>{
276
+ delete process.env.MODE;
277
+ delete process.env.LOG_LEVEL;
278
+ expect(envConfig.getLogConfig().level).toBe('info');
279
+ });
280
+ it('should default log level to debug outside production', ()=>{
281
+ process.env.MODE = 'dev';
282
+ delete process.env.LOG_LEVEL;
283
+ expect(envConfig.getLogConfig().level).toBe('debug');
284
+ });
285
+ it('should apply all defaults when unset', ()=>{
286
+ process.env.MODE = 'dev';
287
+ delete process.env.LOG_DIR;
288
+ delete process.env.LOG_LEVEL;
289
+ delete process.env.LOG_MAX_SIZE;
290
+ delete process.env.LOG_MAX_FILES;
291
+ delete process.env.DISABLE_HTTP_LOGGING;
292
+ expect(envConfig.getLogConfig()).toEqual({
293
+ dir: 'logs',
294
+ level: 'debug',
295
+ maxSize: '20m',
296
+ maxFiles: '14d',
297
+ disableHttpLogging: false
298
+ });
299
+ });
300
+ it('should honor explicit overrides', ()=>{
301
+ process.env.LOG_DIR = '/var/log/flusys';
302
+ process.env.LOG_LEVEL = 'warn';
303
+ process.env.LOG_MAX_SIZE = '50m';
304
+ process.env.LOG_MAX_FILES = '30d';
305
+ process.env.DISABLE_HTTP_LOGGING = 'true';
306
+ expect(envConfig.getLogConfig()).toEqual({
307
+ dir: '/var/log/flusys',
308
+ level: 'warn',
309
+ maxSize: '50m',
310
+ maxFiles: '30d',
311
+ disableHttpLogging: true
312
+ });
313
+ });
314
+ });
315
+ describe('getEnv', ()=>{
316
+ it('should return the full process.env map', ()=>{
317
+ process.env.SOME_FLAG = 'x';
318
+ expect(envConfig.getEnv()).toBe(process.env);
319
+ });
320
+ });
321
+ });