@dismissible/nestjs-api 0.0.2-canary.8976e84.0 → 0.0.2-canary.b0d8bfe.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.

Potentially problematic release.


This version of @dismissible/nestjs-api might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/config/.env.yaml +34 -1
  2. package/jest.config.ts +23 -1
  3. package/jest.e2e-config.ts +2 -1
  4. package/package.json +22 -13
  5. package/project.json +7 -4
  6. package/scripts/performance-test.config.json +29 -0
  7. package/scripts/performance-test.ts +845 -0
  8. package/src/app-test.factory.ts +8 -1
  9. package/src/app.module.ts +13 -1
  10. package/src/app.setup.ts +40 -5
  11. package/src/bootstrap.ts +8 -3
  12. package/src/config/app.config.spec.ts +117 -0
  13. package/src/config/app.config.ts +5 -0
  14. package/src/config/config.module.spec.ts +0 -2
  15. package/src/config/default-app.config.spec.ts +74 -0
  16. package/src/config/default-app.config.ts +15 -0
  17. package/src/cors/cors.config.spec.ts +162 -0
  18. package/src/cors/cors.config.ts +37 -0
  19. package/src/cors/index.ts +1 -0
  20. package/src/health/health.controller.spec.ts +7 -31
  21. package/src/health/health.controller.ts +4 -5
  22. package/src/health/health.module.ts +2 -3
  23. package/src/health/index.ts +0 -1
  24. package/src/helmet/helmet.config.spec.ts +197 -0
  25. package/src/helmet/helmet.config.ts +60 -0
  26. package/src/helmet/index.ts +1 -0
  27. package/src/server/server.config.spec.ts +65 -0
  28. package/src/swagger/swagger.config.spec.ts +113 -0
  29. package/src/swagger/swagger.config.ts +2 -10
  30. package/src/swagger/swagger.factory.spec.ts +125 -0
  31. package/src/swagger/swagger.factory.ts +0 -1
  32. package/src/validation/index.ts +1 -0
  33. package/src/validation/validation.config.ts +47 -0
  34. package/test/config/.env.yaml +23 -0
  35. package/test/config-jwt-auth/.env.yaml +26 -0
  36. package/test/dismiss.e2e-spec.ts +61 -0
  37. package/test/full-cycle.e2e-spec.ts +55 -0
  38. package/test/get-or-create.e2e-spec.ts +51 -0
  39. package/test/jwt-auth.e2e-spec.ts +335 -0
  40. package/test/restore.e2e-spec.ts +61 -0
  41. package/tsconfig.e2e.json +12 -0
  42. package/tsconfig.spec.json +12 -0
  43. package/src/app.e2e-spec.ts +0 -221
  44. package/src/health/health.service.spec.ts +0 -46
  45. package/src/health/health.service.ts +0 -16
@@ -1,5 +1,6 @@
1
1
  import { Test, TestingModuleBuilder } from '@nestjs/testing';
2
2
  import { INestApplication } from '@nestjs/common';
3
+ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
3
4
  import { AppModule, AppModuleOptions } from './app.module';
4
5
  import { configureApp } from './app.setup';
5
6
  import { PrismaService } from '@dismissible/nestjs-postgres-storage';
@@ -19,10 +20,16 @@ export async function createTestApp(options?: TestAppOptions): Promise<INestAppl
19
20
  }
20
21
 
21
22
  const moduleFixture = await builder.compile();
22
- const app = moduleFixture.createNestApplication();
23
+ const app = moduleFixture.createNestApplication<NestFastifyApplication>(
24
+ new FastifyAdapter({
25
+ bodyLimit: 10 * 1024, // 10kb
26
+ }),
27
+ );
23
28
  await configureApp(app);
24
29
  await app.init();
25
30
 
31
+ await app.getHttpAdapter().getInstance().ready();
32
+
26
33
  return app;
27
34
  }
28
35
 
package/src/app.module.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Module, ModuleMetadata, Type } from '@nestjs/common';
1
+ import { DynamicModule, Module, ModuleMetadata, Type } from '@nestjs/common';
2
2
  import { HealthModule } from './health';
3
3
  import { ConfigModule } from './config';
4
4
  import { AppConfig } from './config/app.config';
@@ -7,11 +7,17 @@ import { DismissibleModule } from '@dismissible/nestjs-dismissible';
7
7
  import { IDismissibleLogger } from '@dismissible/nestjs-logger';
8
8
  import { DefaultAppConfig } from './config/default-app.config';
9
9
  import { PostgresStorageConfig, PostgresStorageModule } from '@dismissible/nestjs-postgres-storage';
10
+ import {
11
+ JwtAuthHookModule,
12
+ JwtAuthHook,
13
+ JwtAuthHookConfig,
14
+ } from '@dismissible/nestjs-jwt-auth-hook';
10
15
 
11
16
  export type AppModuleOptions = {
12
17
  configPath?: string;
13
18
  schema?: new () => DefaultAppConfig;
14
19
  logger?: Type<IDismissibleLogger>;
20
+ imports?: DynamicModule[];
15
21
  };
16
22
 
17
23
  @Module({})
@@ -31,8 +37,14 @@ export class AppModule {
31
37
  schema: options?.schema ?? AppConfig,
32
38
  }),
33
39
  HealthModule,
40
+ ...(options?.imports ?? []),
41
+ JwtAuthHookModule.forRootAsync({
42
+ useFactory: (config: JwtAuthHookConfig) => config,
43
+ inject: [JwtAuthHookConfig],
44
+ }),
34
45
  DismissibleModule.forRoot({
35
46
  logger: options?.logger,
47
+ hooks: [JwtAuthHook],
36
48
  storage: PostgresStorageModule.forRootAsync({
37
49
  useFactory(config: PostgresStorageConfig) {
38
50
  return {
package/src/app.setup.ts CHANGED
@@ -1,14 +1,49 @@
1
1
  import { INestApplication, ValidationPipe } from '@nestjs/common';
2
+ import { NestFastifyApplication } from '@nestjs/platform-fastify';
3
+ import fastifyHelmet from '@fastify/helmet';
2
4
  import { SwaggerConfig, configureAppWithSwagger } from './swagger';
5
+ import { CorsConfig } from './cors';
6
+ import { HelmetConfig } from './helmet';
7
+ import { ValidationConfig } from './validation';
3
8
 
4
9
  export async function configureApp(app: INestApplication): Promise<void> {
5
- // Global validation pipe for request validation
10
+ const fastifyApp = app as NestFastifyApplication;
11
+
12
+ const helmetConfig = app.get(HelmetConfig);
13
+ if (helmetConfig.enabled) {
14
+ await fastifyApp.register(fastifyHelmet, {
15
+ contentSecurityPolicy: helmetConfig.contentSecurityPolicy ?? true,
16
+ crossOriginEmbedderPolicy: helmetConfig.crossOriginEmbedderPolicy ?? true,
17
+ hsts: {
18
+ maxAge: helmetConfig.hstsMaxAge ?? 31536000,
19
+ includeSubDomains: helmetConfig.hstsIncludeSubDomains ?? true,
20
+ preload: helmetConfig.hstsPreload ?? false,
21
+ },
22
+ });
23
+ }
24
+
25
+ const corsConfig = app.get(CorsConfig);
26
+ if (corsConfig.enabled) {
27
+ app.enableCors({
28
+ origin: corsConfig.origins ?? ['http://localhost:3001', 'http://localhost:5173'],
29
+ methods: corsConfig.methods ?? ['GET', 'POST', 'DELETE', 'OPTIONS'],
30
+ allowedHeaders: corsConfig.allowedHeaders ?? [
31
+ 'Content-Type',
32
+ 'Authorization',
33
+ 'x-request-id',
34
+ ],
35
+ credentials: corsConfig.credentials ?? true,
36
+ maxAge: corsConfig.maxAge ?? 86400,
37
+ });
38
+ }
39
+
40
+ const validationConfig = app.get(ValidationConfig);
6
41
  app.useGlobalPipes(
7
42
  new ValidationPipe({
8
- whitelist: true,
9
- forbidNonWhitelisted: true,
10
- transform: true,
11
- disableErrorMessages: false,
43
+ whitelist: validationConfig.whitelist ?? true,
44
+ forbidNonWhitelisted: validationConfig.forbidNonWhitelisted ?? true,
45
+ transform: validationConfig.transform ?? true,
46
+ disableErrorMessages: validationConfig.disableErrorMessages ?? true,
12
47
  }),
13
48
  );
14
49
 
package/src/bootstrap.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { NestFactory } from '@nestjs/core';
2
+ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
2
3
  import { AppModule } from './app.module';
3
4
  import { ServerConfig } from './server/server.config';
4
5
  import { configureApp } from './app.setup';
@@ -13,12 +14,16 @@ export type IBootstrapOptions = {
13
14
  };
14
15
 
15
16
  export async function bootstrap(options?: IBootstrapOptions) {
16
- const app = await NestFactory.create(AppModule.forRoot(options));
17
-
17
+ const app = await NestFactory.create<NestFastifyApplication>(
18
+ AppModule.forRoot(options),
19
+ new FastifyAdapter({
20
+ bodyLimit: 10 * 1024, // 10kb
21
+ }),
22
+ );
18
23
  await configureApp(app);
19
24
 
20
25
  const serverConfig = app.get(ServerConfig);
21
26
  const port = serverConfig.port ?? 3000;
22
- await app.listen(port);
27
+ await app.listen(port, '0.0.0.0');
23
28
  console.log(`🚀 Application is running on: http://localhost:${port}`);
24
29
  }
@@ -0,0 +1,117 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { AppConfig } from './app.config';
5
+ import { SwaggerConfig } from '../swagger/swagger.config';
6
+ import { PostgresStorageConfig } from '@dismissible/nestjs-postgres-storage';
7
+ import { JwtAuthHookConfig } from '@dismissible/nestjs-jwt-auth-hook';
8
+
9
+ describe('AppConfig', () => {
10
+ describe('transformation', () => {
11
+ it('should transform all nested configs correctly', () => {
12
+ const config = plainToInstance(AppConfig, {
13
+ server: { port: 3001 },
14
+ cors: { enabled: true },
15
+ helmet: { enabled: true },
16
+ swagger: { enabled: true },
17
+ db: { connectionString: 'postgresql://localhost:5432/test' },
18
+ jwtAuth: {
19
+ enabled: true,
20
+ wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
21
+ },
22
+ });
23
+
24
+ expect(config.swagger).toBeInstanceOf(SwaggerConfig);
25
+ expect(config.swagger.enabled).toBe(true);
26
+ expect(config.db).toBeInstanceOf(PostgresStorageConfig);
27
+ expect(config.db.connectionString).toBe('postgresql://localhost:5432/test');
28
+ expect(config.jwtAuth).toBeInstanceOf(JwtAuthHookConfig);
29
+ expect(config.jwtAuth.enabled).toBe(true);
30
+ expect(config.jwtAuth.wellKnownUrl).toBe(
31
+ 'https://auth.example.com/.well-known/openid-configuration',
32
+ );
33
+ });
34
+ });
35
+
36
+ describe('validation', () => {
37
+ it('should pass validation with all required nested configs', async () => {
38
+ const config = plainToInstance(AppConfig, {
39
+ server: { port: 3001 },
40
+ cors: { enabled: true },
41
+ helmet: { enabled: true },
42
+ swagger: { enabled: true },
43
+ db: { connectionString: 'postgresql://localhost:5432/test' },
44
+ jwtAuth: {
45
+ enabled: true,
46
+ wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
47
+ },
48
+ });
49
+ const errors = await validate(config);
50
+ expect(errors).toHaveLength(0);
51
+ });
52
+
53
+ it('should fail validation when swagger config has invalid nested property', async () => {
54
+ const config = plainToInstance(AppConfig, {
55
+ server: { port: 3001 },
56
+ cors: { enabled: true },
57
+ helmet: { enabled: true },
58
+ swagger: { enabled: true, path: 123 },
59
+ db: { connectionString: 'postgresql://localhost:5432/test' },
60
+ jwtAuth: {
61
+ enabled: true,
62
+ wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
63
+ },
64
+ });
65
+ const errors = await validate(config);
66
+ expect(errors.length).toBeGreaterThan(0);
67
+ const swaggerError = errors.find((e) => e.property === 'swagger');
68
+ expect(swaggerError).toBeDefined();
69
+ });
70
+
71
+ it('should fail validation when db config is invalid', async () => {
72
+ const config = plainToInstance(AppConfig, {
73
+ server: { port: 3001 },
74
+ cors: { enabled: true },
75
+ helmet: { enabled: true },
76
+ swagger: { enabled: true },
77
+ db: { connectionString: 123 },
78
+ jwtAuth: {
79
+ enabled: true,
80
+ wellKnownUrl: 'https://auth.example.com/.well-known/openid-configuration',
81
+ },
82
+ });
83
+ const errors = await validate(config);
84
+ expect(errors.length).toBeGreaterThan(0);
85
+ const dbError = errors.find((e) => e.property === 'db');
86
+ expect(dbError).toBeDefined();
87
+ });
88
+
89
+ it('should fail validation when jwtAuth config is invalid', async () => {
90
+ const config = plainToInstance(AppConfig, {
91
+ server: { port: 3001 },
92
+ cors: { enabled: true },
93
+ helmet: { enabled: true },
94
+ swagger: { enabled: true },
95
+ db: { connectionString: 'postgresql://localhost:5432/test' },
96
+ jwtAuth: { enabled: true, wellKnownUrl: 'not-a-valid-url' },
97
+ });
98
+ const errors = await validate(config);
99
+ expect(errors.length).toBeGreaterThan(0);
100
+ const jwtAuthError = errors.find((e) => e.property === 'jwtAuth');
101
+ expect(jwtAuthError).toBeDefined();
102
+ });
103
+
104
+ it('should fail validation when jwtAuth enabled is false but wellKnownUrl is required', async () => {
105
+ const config = plainToInstance(AppConfig, {
106
+ server: { port: 3001 },
107
+ cors: { enabled: true },
108
+ helmet: { enabled: true },
109
+ swagger: { enabled: true },
110
+ db: { connectionString: 'postgresql://localhost:5432/test' },
111
+ jwtAuth: { enabled: false },
112
+ });
113
+ const errors = await validate(config);
114
+ expect(errors).toHaveLength(0);
115
+ });
116
+ });
117
+ });
@@ -3,6 +3,7 @@ import { Type } from 'class-transformer';
3
3
  import { SwaggerConfig } from '../swagger';
4
4
  import { DefaultAppConfig } from './default-app.config';
5
5
  import { PostgresStorageConfig } from '@dismissible/nestjs-postgres-storage';
6
+ import { JwtAuthHookConfig } from '@dismissible/nestjs-jwt-auth-hook';
6
7
 
7
8
  export class AppConfig extends DefaultAppConfig {
8
9
  @ValidateNested()
@@ -12,4 +13,8 @@ export class AppConfig extends DefaultAppConfig {
12
13
  @ValidateNested()
13
14
  @Type(() => PostgresStorageConfig)
14
15
  public readonly db!: PostgresStorageConfig;
16
+
17
+ @ValidateNested()
18
+ @Type(() => JwtAuthHookConfig)
19
+ public readonly jwtAuth!: JwtAuthHookConfig;
15
20
  }
@@ -2,7 +2,6 @@ import { ConfigModule } from './config.module';
2
2
  import { TypedConfigModule } from 'nest-typed-config';
3
3
  import { fileLoader } from 'nest-typed-config';
4
4
 
5
- // Mock nest-typed-config
6
5
  jest.mock('nest-typed-config', () => ({
7
6
  TypedConfigModule: {
8
7
  forRoot: jest.fn(),
@@ -10,7 +9,6 @@ jest.mock('nest-typed-config', () => ({
10
9
  fileLoader: jest.fn(),
11
10
  }));
12
11
 
13
- // Mock path
14
12
  jest.mock('path', () => ({
15
13
  join: jest.fn((...args) => args.join('/')),
16
14
  }));
@@ -0,0 +1,74 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { DefaultAppConfig } from './default-app.config';
5
+ import { ServerConfig } from '../server/server.config';
6
+ import { CorsConfig } from '../cors/cors.config';
7
+ import { HelmetConfig } from '../helmet/helmet.config';
8
+
9
+ describe('DefaultAppConfig', () => {
10
+ describe('transformation', () => {
11
+ it('should transform all nested configs correctly', () => {
12
+ const config = plainToInstance(DefaultAppConfig, {
13
+ server: { port: 3001 },
14
+ cors: { enabled: true },
15
+ helmet: { enabled: true },
16
+ });
17
+
18
+ expect(config.server).toBeInstanceOf(ServerConfig);
19
+ expect(config.server.port).toBe(3001);
20
+ expect(config.cors).toBeInstanceOf(CorsConfig);
21
+ expect(config.cors.enabled).toBe(true);
22
+ expect(config.helmet).toBeInstanceOf(HelmetConfig);
23
+ expect(config.helmet.enabled).toBe(true);
24
+ });
25
+ });
26
+
27
+ describe('validation', () => {
28
+ it('should pass validation with all required nested configs', async () => {
29
+ const config = plainToInstance(DefaultAppConfig, {
30
+ server: { port: 3001 },
31
+ cors: { enabled: true },
32
+ helmet: { enabled: true },
33
+ });
34
+ const errors = await validate(config);
35
+ expect(errors).toHaveLength(0);
36
+ });
37
+
38
+ it('should fail validation when server config is invalid', async () => {
39
+ const config = plainToInstance(DefaultAppConfig, {
40
+ server: { port: 'not-a-number' },
41
+ cors: { enabled: true },
42
+ helmet: { enabled: true },
43
+ });
44
+ const errors = await validate(config);
45
+ expect(errors.length).toBeGreaterThan(0);
46
+ const serverError = errors.find((e) => e.property === 'server');
47
+ expect(serverError).toBeDefined();
48
+ });
49
+
50
+ it('should fail validation when cors config has invalid nested property', async () => {
51
+ const config = plainToInstance(DefaultAppConfig, {
52
+ server: { port: 3001 },
53
+ cors: { enabled: true, maxAge: 'not-a-number' },
54
+ helmet: { enabled: true },
55
+ });
56
+ const errors = await validate(config);
57
+ expect(errors.length).toBeGreaterThan(0);
58
+ const corsError = errors.find((e) => e.property === 'cors');
59
+ expect(corsError).toBeDefined();
60
+ });
61
+
62
+ it('should fail validation when helmet config has invalid nested property', async () => {
63
+ const config = plainToInstance(DefaultAppConfig, {
64
+ server: { port: 3001 },
65
+ cors: { enabled: true },
66
+ helmet: { enabled: true, hstsMaxAge: 'not-a-number' },
67
+ });
68
+ const errors = await validate(config);
69
+ expect(errors.length).toBeGreaterThan(0);
70
+ const helmetError = errors.find((e) => e.property === 'helmet');
71
+ expect(helmetError).toBeDefined();
72
+ });
73
+ });
74
+ });
@@ -1,9 +1,24 @@
1
1
  import { ValidateNested } from 'class-validator';
2
2
  import { Type } from 'class-transformer';
3
3
  import { ServerConfig } from '../server/server.config';
4
+ import { CorsConfig } from '../cors';
5
+ import { HelmetConfig } from '../helmet';
6
+ import { ValidationConfig } from '../validation';
4
7
 
5
8
  export class DefaultAppConfig {
6
9
  @ValidateNested()
7
10
  @Type(() => ServerConfig)
8
11
  public readonly server!: ServerConfig;
12
+
13
+ @ValidateNested()
14
+ @Type(() => CorsConfig)
15
+ public readonly cors!: CorsConfig;
16
+
17
+ @ValidateNested()
18
+ @Type(() => HelmetConfig)
19
+ public readonly helmet!: HelmetConfig;
20
+
21
+ @ValidateNested()
22
+ @Type(() => ValidationConfig)
23
+ public readonly validation!: ValidationConfig;
9
24
  }
@@ -0,0 +1,162 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { CorsConfig } from './cors.config';
5
+
6
+ describe('CorsConfig', () => {
7
+ describe('enabled', () => {
8
+ it('should transform string "true" to boolean true', () => {
9
+ const config = plainToInstance(CorsConfig, { enabled: 'true' });
10
+ expect(config.enabled).toBe(true);
11
+ });
12
+
13
+ it('should transform string "false" to boolean false', () => {
14
+ const config = plainToInstance(CorsConfig, { enabled: 'false' });
15
+ expect(config.enabled).toBe(false);
16
+ });
17
+
18
+ it('should keep boolean true as true', () => {
19
+ const config = plainToInstance(CorsConfig, { enabled: true });
20
+ expect(config.enabled).toBe(true);
21
+ });
22
+
23
+ it('should keep boolean false as false', () => {
24
+ const config = plainToInstance(CorsConfig, { enabled: false });
25
+ expect(config.enabled).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe('origins', () => {
30
+ it('should transform comma-separated string to array', () => {
31
+ const config = plainToInstance(CorsConfig, {
32
+ enabled: true,
33
+ origins: 'http://localhost:3000,http://localhost:4000',
34
+ });
35
+ expect(config.origins).toEqual(['http://localhost:3000', 'http://localhost:4000']);
36
+ });
37
+
38
+ it('should trim whitespace from origins', () => {
39
+ const config = plainToInstance(CorsConfig, {
40
+ enabled: true,
41
+ origins: 'http://localhost:3000 , http://localhost:4000 ',
42
+ });
43
+ expect(config.origins).toEqual(['http://localhost:3000', 'http://localhost:4000']);
44
+ });
45
+
46
+ it('should handle single origin string', () => {
47
+ const config = plainToInstance(CorsConfig, {
48
+ enabled: true,
49
+ origins: 'http://localhost:3000',
50
+ });
51
+ expect(config.origins).toEqual(['http://localhost:3000']);
52
+ });
53
+
54
+ it('should keep array as array', () => {
55
+ const config = plainToInstance(CorsConfig, {
56
+ enabled: true,
57
+ origins: ['http://localhost:3000', 'http://localhost:4000'],
58
+ });
59
+ expect(config.origins).toEqual(['http://localhost:3000', 'http://localhost:4000']);
60
+ });
61
+ });
62
+
63
+ describe('methods', () => {
64
+ it('should transform comma-separated string to array', () => {
65
+ const config = plainToInstance(CorsConfig, {
66
+ enabled: true,
67
+ methods: 'GET,POST,DELETE',
68
+ });
69
+ expect(config.methods).toEqual(['GET', 'POST', 'DELETE']);
70
+ });
71
+
72
+ it('should trim whitespace from methods', () => {
73
+ const config = plainToInstance(CorsConfig, {
74
+ enabled: true,
75
+ methods: 'GET , POST , DELETE',
76
+ });
77
+ expect(config.methods).toEqual(['GET', 'POST', 'DELETE']);
78
+ });
79
+ });
80
+
81
+ describe('allowedHeaders', () => {
82
+ it('should transform comma-separated string to array', () => {
83
+ const config = plainToInstance(CorsConfig, {
84
+ enabled: true,
85
+ allowedHeaders: 'Content-Type,Authorization,x-request-id',
86
+ });
87
+ expect(config.allowedHeaders).toEqual(['Content-Type', 'Authorization', 'x-request-id']);
88
+ });
89
+
90
+ it('should trim whitespace from headers', () => {
91
+ const config = plainToInstance(CorsConfig, {
92
+ enabled: true,
93
+ allowedHeaders: 'Content-Type , Authorization , x-request-id',
94
+ });
95
+ expect(config.allowedHeaders).toEqual(['Content-Type', 'Authorization', 'x-request-id']);
96
+ });
97
+ });
98
+
99
+ describe('credentials', () => {
100
+ it('should transform string "true" to boolean true', () => {
101
+ const config = plainToInstance(CorsConfig, { enabled: true, credentials: 'true' });
102
+ expect(config.credentials).toBe(true);
103
+ });
104
+
105
+ it('should transform string "false" to boolean false', () => {
106
+ const config = plainToInstance(CorsConfig, { enabled: true, credentials: 'false' });
107
+ expect(config.credentials).toBe(false);
108
+ });
109
+
110
+ it('should keep boolean values unchanged', () => {
111
+ const configTrue = plainToInstance(CorsConfig, { enabled: true, credentials: true });
112
+ const configFalse = plainToInstance(CorsConfig, { enabled: true, credentials: false });
113
+ expect(configTrue.credentials).toBe(true);
114
+ expect(configFalse.credentials).toBe(false);
115
+ });
116
+ });
117
+
118
+ describe('maxAge', () => {
119
+ it('should transform string to number', () => {
120
+ const config = plainToInstance(CorsConfig, { enabled: true, maxAge: '86400' });
121
+ expect(config.maxAge).toBe(86400);
122
+ });
123
+
124
+ it('should keep number unchanged', () => {
125
+ const config = plainToInstance(CorsConfig, { enabled: true, maxAge: 3600 });
126
+ expect(config.maxAge).toBe(3600);
127
+ });
128
+ });
129
+
130
+ describe('validation', () => {
131
+ it('should pass validation with valid config', async () => {
132
+ const config = plainToInstance(CorsConfig, {
133
+ enabled: true,
134
+ origins: ['http://localhost:3000'],
135
+ methods: ['GET', 'POST'],
136
+ allowedHeaders: ['Content-Type'],
137
+ credentials: true,
138
+ maxAge: 86400,
139
+ });
140
+
141
+ const errors = await validate(config);
142
+ expect(errors).toHaveLength(0);
143
+ });
144
+
145
+ it('should pass validation with only required fields', async () => {
146
+ const config = plainToInstance(CorsConfig, {
147
+ enabled: true,
148
+ });
149
+
150
+ const errors = await validate(config);
151
+ expect(errors).toHaveLength(0);
152
+ });
153
+
154
+ it('should fail validation when enabled is missing', async () => {
155
+ const config = plainToInstance(CorsConfig, {});
156
+
157
+ const errors = await validate(config);
158
+ expect(errors.length).toBeGreaterThan(0);
159
+ expect(errors[0].property).toBe('enabled');
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,37 @@
1
+ import { IsArray, IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';
2
+ import { Type } from 'class-transformer';
3
+ import { TransformBoolean, TransformCommaSeparated } from '@dismissible/nestjs-validation';
4
+
5
+ export class CorsConfig {
6
+ @IsBoolean()
7
+ @TransformBoolean()
8
+ public readonly enabled!: boolean;
9
+
10
+ @IsArray()
11
+ @IsString({ each: true })
12
+ @IsOptional()
13
+ @TransformCommaSeparated()
14
+ public readonly origins?: string[];
15
+
16
+ @IsArray()
17
+ @IsString({ each: true })
18
+ @IsOptional()
19
+ @TransformCommaSeparated()
20
+ public readonly methods?: string[];
21
+
22
+ @IsArray()
23
+ @IsString({ each: true })
24
+ @IsOptional()
25
+ @TransformCommaSeparated()
26
+ public readonly allowedHeaders?: string[];
27
+
28
+ @IsBoolean()
29
+ @IsOptional()
30
+ @TransformBoolean()
31
+ public readonly credentials?: boolean;
32
+
33
+ @IsNumber()
34
+ @IsOptional()
35
+ @Type(() => Number)
36
+ public readonly maxAge?: number;
37
+ }
@@ -0,0 +1 @@
1
+ export * from './cors.config';
@@ -1,48 +1,24 @@
1
- import { mock, Mock } from 'ts-jest-mocker';
2
1
  import { HealthController } from './health.controller';
3
- import { HealthService } from './health.service';
4
2
 
5
3
  describe('HealthController', () => {
6
4
  let controller: HealthController;
7
- let healthService: Mock<HealthService>;
8
5
 
9
6
  beforeEach(() => {
10
- healthService = mock<HealthService>({
11
- failIfMockNotProvided: false,
12
- });
13
- controller = new HealthController(healthService);
7
+ jest.useFakeTimers();
8
+ jest.setSystemTime(new Date('2024-01-20T15:30:00.000Z'));
9
+ controller = new HealthController();
14
10
  });
15
11
 
16
- describe('getHealth', () => {
17
- it('should return health status from service', () => {
18
- const mockHealth = {
19
- status: 'ok',
20
- timestamp: '2024-01-15T10:00:00.000Z',
21
- uptime: 123.45,
22
- };
23
-
24
- healthService.getHealth.mockReturnValue(mockHealth);
25
-
26
- const result = controller.getHealth();
27
-
28
- expect(result).toEqual(mockHealth);
29
- expect(healthService.getHealth).toHaveBeenCalledTimes(1);
30
- });
12
+ afterEach(() => {
13
+ jest.useRealTimers();
14
+ });
31
15
 
16
+ describe('getHealth', () => {
32
17
  it('should return the exact response from service', () => {
33
- const mockHealth = {
34
- status: 'ok',
35
- timestamp: '2024-01-20T15:30:00.000Z',
36
- uptime: 999.99,
37
- };
38
-
39
- healthService.getHealth.mockReturnValue(mockHealth);
40
-
41
18
  const result = controller.getHealth();
42
19
 
43
20
  expect(result.status).toBe('ok');
44
21
  expect(result.timestamp).toBe('2024-01-20T15:30:00.000Z');
45
- expect(result.uptime).toBe(999.99);
46
22
  });
47
23
  });
48
24
  });
@@ -1,17 +1,16 @@
1
1
  import { Controller, Get, HttpCode, HttpStatus } from '@nestjs/common';
2
- import { HealthService } from './health.service';
3
2
 
4
3
  @Controller('health')
5
4
  export class HealthController {
6
- constructor(private readonly healthService: HealthService) {}
7
-
8
5
  @Get()
9
6
  @HttpCode(HttpStatus.OK)
10
7
  getHealth(): {
11
8
  status: string;
12
9
  timestamp: string;
13
- uptime: number;
14
10
  } {
15
- return this.healthService.getHealth();
11
+ return {
12
+ status: 'ok',
13
+ timestamp: new Date().toISOString(),
14
+ };
16
15
  }
17
16
  }