@dismissible/nestjs-api 0.0.2-canary.c91edbc.0 → 0.0.2-canary.d2f56d7.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.
package/config/.env.yaml CHANGED
@@ -14,11 +14,28 @@ helmet:
14
14
 
15
15
  cors:
16
16
  enabled: ${DISMISSIBLE_CORS_ENABLED:-true}
17
- origins: ${DISMISSIBLE_CORS_ORIGINS:-http://localhost:3000}
17
+ origins: ${DISMISSIBLE_CORS_ORIGINS:-http://localhost:3001,http://localhost:5173}
18
18
  methods: ${DISMISSIBLE_CORS_METHODS:-GET,POST,DELETE,OPTIONS}
19
19
  allowedHeaders: ${DISMISSIBLE_CORS_ALLOWED_HEADERS:-Content-Type,Authorization,x-request-id}
20
20
  credentials: ${DISMISSIBLE_CORS_CREDENTIALS:-true}
21
21
  maxAge: ${DISMISSIBLE_CORS_MAX_AGE:-86400}
22
22
 
23
23
  db:
24
- connectionString: ${DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING:-'postgresql://postgres:postgres@localhost:5432/dismissible'}
24
+ connectionString: ${DISMISSIBLE_POSTGRES_STORAGE_CONNECTION_STRING:-postgresql://postgres:postgres@localhost:5432/dismissible}
25
+
26
+ jwtAuth:
27
+ enabled: ${DISMISSIBLE_JWT_AUTH_ENABLED:-false}
28
+ wellKnownUrl: ${DISMISSIBLE_JWT_AUTH_WELL_KNOWN_URL:-}
29
+ issuer: ${DISMISSIBLE_JWT_AUTH_ISSUER:-}
30
+ audience: ${DISMISSIBLE_JWT_AUTH_AUDIENCE:-}
31
+ algorithms:
32
+ - ${DISMISSIBLE_JWT_AUTH_ALGORITHMS:-RS256}
33
+ jwksCacheDuration: ${DISMISSIBLE_JWT_AUTH_JWKS_CACHE_DURATION:-600000}
34
+ requestTimeout: ${DISMISSIBLE_JWT_AUTH_REQUEST_TIMEOUT:-30000}
35
+ priority: ${DISMISSIBLE_JWT_AUTH_PRIORITY:--100}
36
+
37
+ validation:
38
+ disableErrorMessages: ${DISMISSIBLE_VALIDATION_DISABLE_ERROR_MESSAGES:-true}
39
+ whitelist: ${DISMISSIBLE_VALIDATION_WHITELIST:-true}
40
+ forbidNonWhitelisted: ${DISMISSIBLE_VALIDATION_FORBID_NON_WHITELISTED:-true}
41
+ transform: ${DISMISSIBLE_VALIDATION_TRANSFORM:-true}
package/jest.config.ts CHANGED
@@ -3,11 +3,33 @@ export default {
3
3
  preset: '../jest.preset.js',
4
4
  testEnvironment: 'node',
5
5
  transform: {
6
- '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
6
+ '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
7
7
  },
8
8
  moduleFileExtensions: ['ts', 'js', 'html'],
9
9
  coverageDirectory: '../coverage/api',
10
10
  testMatch: ['**/*.spec.ts'],
11
11
  testPathIgnorePatterns: ['\\.e2e-spec\\.ts$'],
12
12
  transformIgnorePatterns: ['node_modules/(?!(nest-typed-config|uuid)/)'],
13
+ collectCoverageFrom: [
14
+ 'src/**/*.ts',
15
+ '!src/**/*.spec.ts',
16
+ '!src/**/*.e2e-spec.ts',
17
+ '!src/**/*.interface.ts',
18
+ '!src/**/*.dto.ts',
19
+ '!src/**/*.enum.ts',
20
+ '!src/**/index.ts',
21
+ '!src/**/*.module.ts',
22
+ '!src/main.ts',
23
+ '!src/bootstrap.ts',
24
+ '!src/app.setup.ts',
25
+ '!src/app-test.factory.ts',
26
+ ],
27
+ coverageThreshold: {
28
+ global: {
29
+ branches: 80,
30
+ functions: 80,
31
+ lines: 80,
32
+ statements: 80,
33
+ },
34
+ },
13
35
  };
@@ -3,10 +3,12 @@ export default {
3
3
  preset: '../jest.preset.js',
4
4
  testEnvironment: 'node',
5
5
  transform: {
6
- '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.json' }],
6
+ '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.e2e.json' }],
7
7
  },
8
8
  moduleFileExtensions: ['ts', 'js', 'html'],
9
9
  coverageDirectory: '../coverage/api-e2e',
10
10
  testMatch: ['**/*.e2e-spec.ts'],
11
11
  transformIgnorePatterns: ['node_modules/(?!(nest-typed-config|uuid)/)'],
12
+ // Run tests sequentially to avoid database conflicts
13
+ maxWorkers: 1,
12
14
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dismissible/nestjs-api",
3
- "version": "0.0.2-canary.c91edbc.0",
3
+ "version": "0.0.2-canary.d2f56d7.0",
4
4
  "description": "Dismissible API application",
5
5
  "main": "./src/main.js",
6
6
  "types": "./src/main.d.ts",
@@ -15,14 +15,20 @@
15
15
  "dismissible-api": "./src/main.js"
16
16
  },
17
17
  "scripts": {
18
- "start": "node src/main.js"
18
+ "start": "node src/main.js",
19
+ "prisma:generate": "npx dismissible-prisma generate",
20
+ "prisma:migrate:deploy": "npx dismissible-prisma migrate deploy",
21
+ "prisma:migrate:dev": "npx dismissible-prisma migrate dev",
22
+ "prisma:db:push": "npx dismissible-prisma db push",
23
+ "prisma:studio": "npx dismissible-prisma studio"
19
24
  },
20
25
  "dependencies": {
21
- "@dismissible/nestjs-dismissible": "^0.0.2-canary.c91edbc.0",
22
- "@dismissible/nestjs-dismissible-item": "^0.0.2-canary.c91edbc.0",
23
- "@dismissible/nestjs-storage": "^0.0.2-canary.c91edbc.0",
24
- "@dismissible/nestjs-postgres-storage": "^0.0.2-canary.c91edbc.0",
25
- "@dismissible/nestjs-logger": "^0.0.2-canary.c91edbc.0",
26
+ "@dismissible/nestjs-dismissible": "^0.0.2-canary.d2f56d7.0",
27
+ "@dismissible/nestjs-dismissible-item": "^0.0.2-canary.d2f56d7.0",
28
+ "@dismissible/nestjs-jwt-auth-hook": "^0.0.1",
29
+ "@dismissible/nestjs-storage": "^0.0.2-canary.d2f56d7.0",
30
+ "@dismissible/nestjs-postgres-storage": "^0.0.2-canary.d2f56d7.0",
31
+ "@dismissible/nestjs-logger": "^0.0.2-canary.d2f56d7.0",
26
32
  "@nestjs/common": "^11.1.9",
27
33
  "@nestjs/core": "^11.1.9",
28
34
  "@nestjs/platform-fastify": "^11.1.9",
package/project.json CHANGED
@@ -65,19 +65,22 @@
65
65
  "prisma:generate": {
66
66
  "executor": "nx:run-commands",
67
67
  "options": {
68
- "command": "npx prisma generate --schema=libs/postgres-storage/prisma/schema.prisma"
68
+ "command": "npm run prisma:generate",
69
+ "cwd": "api"
69
70
  }
70
71
  },
71
72
  "prisma:migrate": {
72
73
  "executor": "nx:run-commands",
73
74
  "options": {
74
- "command": "npx prisma migrate dev --schema=libs/postgres-storage/prisma/schema.prisma"
75
+ "command": "npm run prisma:migrate:dev",
76
+ "cwd": "api"
75
77
  }
76
78
  },
77
79
  "prisma:push": {
78
80
  "executor": "nx:run-commands",
79
81
  "options": {
80
- "command": "npx prisma db push --schema=libs/postgres-storage/prisma/schema.prisma"
82
+ "command": "npm run prisma:db:push",
83
+ "cwd": "api"
81
84
  }
82
85
  },
83
86
  "npm-publish": {
@@ -673,7 +673,7 @@ async function runCreatePhase(
673
673
  for (let i = 0; i < config.createCount; i++) {
674
674
  const userId = userIds[i % userIds.length];
675
675
  const itemId = itemIds[i];
676
- paths.push(`/v1/user/${userId}/dismissible-item/${itemId}`);
676
+ paths.push(`/v1/users/${userId}/items/${itemId}`);
677
677
  }
678
678
 
679
679
  const instance = autocannon(
@@ -741,7 +741,7 @@ async function runGetPhase(
741
741
  for (let i = 0; i < itemsToGet; i++) {
742
742
  const userId = userIds[i % userIds.length];
743
743
  const itemId = itemIds[i];
744
- paths.push(`/v1/user/${userId}/dismissible-item/${itemId}`);
744
+ paths.push(`/v1/users/${userId}/items/${itemId}`);
745
745
  }
746
746
 
747
747
  const instance = autocannon(
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
@@ -4,6 +4,7 @@ import fastifyHelmet from '@fastify/helmet';
4
4
  import { SwaggerConfig, configureAppWithSwagger } from './swagger';
5
5
  import { CorsConfig } from './cors';
6
6
  import { HelmetConfig } from './helmet';
7
+ import { ValidationConfig } from './validation';
7
8
 
8
9
  export async function configureApp(app: INestApplication): Promise<void> {
9
10
  const fastifyApp = app as NestFastifyApplication;
@@ -26,7 +27,7 @@ export async function configureApp(app: INestApplication): Promise<void> {
26
27
  const corsConfig = app.get(CorsConfig);
27
28
  if (corsConfig.enabled) {
28
29
  app.enableCors({
29
- origin: corsConfig.origins ?? ['http://localhost:3000'],
30
+ origin: corsConfig.origins ?? ['http://localhost:3001', 'http://localhost:5173'],
30
31
  methods: corsConfig.methods ?? ['GET', 'POST', 'DELETE', 'OPTIONS'],
31
32
  allowedHeaders: corsConfig.allowedHeaders ?? [
32
33
  'Content-Type',
@@ -39,12 +40,13 @@ export async function configureApp(app: INestApplication): Promise<void> {
39
40
  }
40
41
 
41
42
  // Global validation pipe for request validation
43
+ const validationConfig = app.get(ValidationConfig);
42
44
  app.useGlobalPipes(
43
45
  new ValidationPipe({
44
- whitelist: true,
45
- forbidNonWhitelisted: true,
46
- transform: true,
47
- disableErrorMessages: false,
46
+ whitelist: validationConfig.whitelist ?? true,
47
+ forbidNonWhitelisted: validationConfig.forbidNonWhitelisted ?? true,
48
+ transform: validationConfig.transform ?? true,
49
+ disableErrorMessages: validationConfig.disableErrorMessages ?? true,
48
50
  }),
49
51
  );
50
52
 
@@ -0,0 +1,118 @@
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
+ // When enabled is false, wellKnownUrl is not required due to ValidateIf
115
+ expect(errors).toHaveLength(0);
116
+ });
117
+ });
118
+ });
@@ -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
  }
@@ -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
+ });
@@ -3,6 +3,7 @@ import { Type } from 'class-transformer';
3
3
  import { ServerConfig } from '../server/server.config';
4
4
  import { CorsConfig } from '../cors';
5
5
  import { HelmetConfig } from '../helmet';
6
+ import { ValidationConfig } from '../validation';
6
7
 
7
8
  export class DefaultAppConfig {
8
9
  @ValidateNested()
@@ -16,4 +17,8 @@ export class DefaultAppConfig {
16
17
  @ValidateNested()
17
18
  @Type(() => HelmetConfig)
18
19
  public readonly helmet!: HelmetConfig;
20
+
21
+ @ValidateNested()
22
+ @Type(() => ValidationConfig)
23
+ public readonly validation!: ValidationConfig;
19
24
  }
@@ -1,6 +1,6 @@
1
1
  import { IsArray, IsBoolean, IsNumber, IsOptional, IsString } from 'class-validator';
2
2
  import { Type } from 'class-transformer';
3
- import { TransformBoolean, TransformCommaSeparated } from '../utils';
3
+ import { TransformBoolean, TransformCommaSeparated } from '@dismissible/nestjs-validation';
4
4
 
5
5
  export class CorsConfig {
6
6
  @IsBoolean()
@@ -1,6 +1,6 @@
1
1
  import { IsBoolean, IsNumber, IsOptional } from 'class-validator';
2
2
  import { Type } from 'class-transformer';
3
- import { TransformBoolean } from '../utils';
3
+ import { TransformBoolean } from '@dismissible/nestjs-validation';
4
4
 
5
5
  /**
6
6
  * @see https://helmetjs.github.io/
@@ -0,0 +1,65 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { ServerConfig } from './server.config';
5
+
6
+ describe('ServerConfig', () => {
7
+ describe('port', () => {
8
+ it('should transform string to number', () => {
9
+ const config = plainToInstance(ServerConfig, { port: '3001' });
10
+ expect(config.port).toBe(3001);
11
+ });
12
+
13
+ it('should keep number unchanged', () => {
14
+ const config = plainToInstance(ServerConfig, { port: 3001 });
15
+ expect(config.port).toBe(3001);
16
+ });
17
+
18
+ it('should handle different port numbers', () => {
19
+ const config = plainToInstance(ServerConfig, { port: '8080' });
20
+ expect(config.port).toBe(8080);
21
+ });
22
+ });
23
+
24
+ describe('validation', () => {
25
+ it('should pass validation with valid port number', async () => {
26
+ const config = plainToInstance(ServerConfig, { port: 3001 });
27
+ const errors = await validate(config);
28
+ expect(errors).toHaveLength(0);
29
+ });
30
+
31
+ it('should pass validation with string port that transforms to number', async () => {
32
+ const config = plainToInstance(ServerConfig, { port: '3001' });
33
+ const errors = await validate(config);
34
+ expect(errors).toHaveLength(0);
35
+ });
36
+
37
+ it('should fail validation when port is missing', async () => {
38
+ const config = plainToInstance(ServerConfig, {});
39
+ const errors = await validate(config);
40
+ expect(errors.length).toBeGreaterThan(0);
41
+ expect(errors[0].property).toBe('port');
42
+ });
43
+
44
+ it('should fail validation when port is not a number', async () => {
45
+ const config = plainToInstance(ServerConfig, { port: 'not-a-number' });
46
+ const errors = await validate(config);
47
+ expect(errors.length).toBeGreaterThan(0);
48
+ expect(errors[0].property).toBe('port');
49
+ });
50
+
51
+ it('should fail validation when port is null', async () => {
52
+ const config = plainToInstance(ServerConfig, { port: null });
53
+ const errors = await validate(config);
54
+ expect(errors.length).toBeGreaterThan(0);
55
+ expect(errors[0].property).toBe('port');
56
+ });
57
+
58
+ it('should fail validation when port is undefined', async () => {
59
+ const config = plainToInstance(ServerConfig, { port: undefined });
60
+ const errors = await validate(config);
61
+ expect(errors.length).toBeGreaterThan(0);
62
+ expect(errors[0].property).toBe('port');
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,113 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { SwaggerConfig } from './swagger.config';
5
+
6
+ describe('SwaggerConfig', () => {
7
+ describe('enabled', () => {
8
+ it('should transform string "true" to boolean true', () => {
9
+ const config = plainToInstance(SwaggerConfig, { enabled: 'true' });
10
+ expect(config.enabled).toBe(true);
11
+ });
12
+
13
+ it('should transform string "false" to boolean false', () => {
14
+ const config = plainToInstance(SwaggerConfig, { enabled: 'false' });
15
+ expect(config.enabled).toBe(false);
16
+ });
17
+
18
+ it('should keep boolean true as true', () => {
19
+ const config = plainToInstance(SwaggerConfig, { enabled: true });
20
+ expect(config.enabled).toBe(true);
21
+ });
22
+
23
+ it('should keep boolean false as false', () => {
24
+ const config = plainToInstance(SwaggerConfig, { enabled: false });
25
+ expect(config.enabled).toBe(false);
26
+ });
27
+
28
+ it('should transform other string values to false', () => {
29
+ const config = plainToInstance(SwaggerConfig, { enabled: 'yes' });
30
+ expect(config.enabled).toBe(false);
31
+ });
32
+
33
+ it('should handle case-insensitive "true"', () => {
34
+ const config = plainToInstance(SwaggerConfig, { enabled: 'TRUE' });
35
+ expect(config.enabled).toBe(true);
36
+ });
37
+
38
+ it('should handle case-insensitive "True"', () => {
39
+ const config = plainToInstance(SwaggerConfig, { enabled: 'True' });
40
+ expect(config.enabled).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe('path', () => {
45
+ it('should accept string path', () => {
46
+ const config = plainToInstance(SwaggerConfig, {
47
+ enabled: true,
48
+ path: 'api-docs',
49
+ });
50
+ expect(config.path).toBe('api-docs');
51
+ });
52
+
53
+ it('should allow path to be optional', () => {
54
+ const config = plainToInstance(SwaggerConfig, { enabled: true });
55
+ expect(config.path).toBeUndefined();
56
+ });
57
+ });
58
+
59
+ describe('validation', () => {
60
+ it('should pass validation with valid config', async () => {
61
+ const config = plainToInstance(SwaggerConfig, {
62
+ enabled: true,
63
+ path: 'docs',
64
+ });
65
+ const errors = await validate(config);
66
+ expect(errors).toHaveLength(0);
67
+ });
68
+
69
+ it('should pass validation with only required fields', async () => {
70
+ const config = plainToInstance(SwaggerConfig, { enabled: true });
71
+ const errors = await validate(config);
72
+ expect(errors).toHaveLength(0);
73
+ });
74
+
75
+ it('should pass validation when enabled is transformed from string', async () => {
76
+ const config = plainToInstance(SwaggerConfig, { enabled: 'true' });
77
+ const errors = await validate(config);
78
+ expect(errors).toHaveLength(0);
79
+ });
80
+
81
+ it('should fail validation when enabled is missing', async () => {
82
+ const config = plainToInstance(SwaggerConfig, {});
83
+ const errors = await validate(config);
84
+ expect(errors.length).toBeGreaterThan(0);
85
+ expect(errors[0].property).toBe('enabled');
86
+ });
87
+
88
+ it('should fail validation when enabled is null', async () => {
89
+ const config = plainToInstance(SwaggerConfig, { enabled: null });
90
+ const errors = await validate(config);
91
+ expect(errors.length).toBeGreaterThan(0);
92
+ expect(errors[0].property).toBe('enabled');
93
+ });
94
+
95
+ it('should fail validation when enabled is undefined', async () => {
96
+ const config = plainToInstance(SwaggerConfig, { enabled: undefined });
97
+ const errors = await validate(config);
98
+ expect(errors.length).toBeGreaterThan(0);
99
+ expect(errors[0].property).toBe('enabled');
100
+ });
101
+
102
+ it('should fail validation when path is not a string', async () => {
103
+ const config = plainToInstance(SwaggerConfig, {
104
+ enabled: true,
105
+ path: 123,
106
+ });
107
+ const errors = await validate(config);
108
+ expect(errors.length).toBeGreaterThan(0);
109
+ const pathError = errors.find((e) => e.property === 'path');
110
+ expect(pathError).toBeDefined();
111
+ });
112
+ });
113
+ });