Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ jobs:
run: yarn tsc

- name: Run Trivy vulnerability scanner in fs mode
uses: aquasecurity/trivy-action@0.33.1
uses: aquasecurity/trivy-action@0.35.0
with:
scan-type: 'fs'
scan-ref: '.'
ignore-unfixed: true
vuln-type: 'os,library'
severity: 'CRITICAL,HIGH'

8 changes: 8 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,13 @@ export default [
rules: {
'@typescript-eslint/indent': 'off'
}
},
{
files: ['**/*.spec.ts'],
rules: {
'max-classes-per-file': 'off',
'functional/immutable-data': 'off',
'@typescript-eslint/unbound-method': 'off'
}
}
]
46 changes: 46 additions & 0 deletions lib/decorators/config.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { registry } from 'lib/module/constants'
import { Config } from './config.decorator'

beforeEach(() => {
registry.clear()
})

describe('@Config decorator', () => {
it('registers a class in the registry', () => {
@Config()
class TestConfig {}

expect(registry.has(TestConfig)).toBe(true)
})

it('sets default registry entry fields', () => {
@Config()
class DefaultsConfig {}

const entry = registry.get(DefaultsConfig)

expect(entry?.base).toBe(DefaultsConfig)
expect(entry?.dependencies).toEqual([])
expect(entry?.resolvedDependencies).toEqual([])
expect(entry?.propertyNameTranslations).toEqual({})
expect(entry?.instance).toBeNull()
})

it('stores transform options when provided', () => {
@Config({ toClassOnly: true })
class TransformConfig {}

const entry = registry.get(TransformConfig)

expect(entry?.transformOptions).toEqual({ toClassOnly: true })
})

it('stores undefined transform options when none provided', () => {
@Config()
class NoTransformConfig {}

const entry = registry.get(NoTransformConfig)

expect(entry?.transformOptions).toBeUndefined()
})
})
76 changes: 76 additions & 0 deletions lib/decorators/env.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { IsString } from 'class-validator'
import { registry } from 'lib/module/constants'
import { getConfigInstance } from 'lib/module/utils'
import { Config } from './config.decorator'
import { Env } from './env.decorator'

beforeEach(() => {
registry.clear()
})

describe('@Env decorator', () => {
it('registers property name translation', () => {
@Config()
class EnvTestConfig {
@Env('NODE_ENV')
readonly environment: string
}

const entry = registry.get(EnvTestConfig)

expect(entry?.propertyNameTranslations).toEqual({
environment: 'NODE_ENV'
})
})

it('registers multiple property name translations', () => {
@Config()
class MultiEnvConfig {
@Env('APP_HOST')
readonly host: string

@Env('APP_PORT')
readonly port: number
}

const entry = registry.get(MultiEnvConfig)

expect(entry?.propertyNameTranslations).toEqual({
host: 'APP_HOST',
port: 'APP_PORT'
})
})

it('registers the class in the registry if not already registered', () => {
// @Env calls registerConfigDefaults internally
class ImplicitConfig {
@Env('SOME_VAR')
readonly value: string
}

expect(registry.has(ImplicitConfig)).toBe(true)

const entry = registry.get(ImplicitConfig)

expect(entry?.propertyNameTranslations).toEqual({
value: 'SOME_VAR'
})
})

it('resolves the actual env value through @Env translation', () => {
process.env.SOME_VAR = 'hello-from-env'

@Config()
class ResolvedEnvConfig {
@IsString()
@Env('SOME_VAR')
readonly value: string
}

const instance = getConfigInstance(ResolvedEnvConfig)

expect(instance.value).toBe('hello-from-env')

delete process.env.SOME_VAR
})
})
111 changes: 111 additions & 0 deletions lib/exceptions/validation.exception.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { ValidationError } from 'class-validator'
import { ValidationException } from './validation.exception'

describe('ValidationException', () => {
it('formats a single validation error with constraint', () => {
const errors: Array<ValidationError> = [
{
property: 'PORT',
constraints: { isInt: 'PORT must be an integer number' },
value: 'abc',
target: {},
children: []
}
]

const exception = new ValidationException('TestConfig', errors)

expect(exception).toBeInstanceOf(Error)
expect(exception.message).toContain('TestConfig')
expect(exception.message).toContain('PORT must be an integer number')
expect(exception.message).toContain('was: abc')
})

it('formats multiple validation errors', () => {
const errors: Array<ValidationError> = [
{
property: 'HOST',
constraints: { isString: 'HOST must be a string' },
value: undefined,
target: {},
children: []
},
{
property: 'PORT',
constraints: { isInt: 'PORT must be an integer number' },
value: 'invalid',
target: {},
children: []
}
]

const exception = new ValidationException('AppConfig', errors)

expect(exception.message).toContain('HOST must be a string')
expect(exception.message).toContain('was: undefined')
expect(exception.message).toContain('PORT must be an integer number')
expect(exception.message).toContain('was: invalid')
})

it('handles empty string constraint with a fallback message', () => {
const errors: Array<ValidationError> = [
{
property: 'UNKNOWN_PROP',
constraints: { custom: '' },
target: {},
children: []
}
]

const exception = new ValidationException('TestConfig', errors)

expect(exception.message).toContain('UNKNOWN_PROP failed for unknown reason or constraint')
})

it('handles undefined constraints gracefully', () => {
const errors: Array<ValidationError> = [
{
property: 'PROP',
constraints: undefined,
target: {},
children: []
}
]

const exception = new ValidationException('TestConfig', errors)

expect(exception.message).toContain('TestConfig')
})

it('shows "was: undefined" for missing values', () => {
const errors: Array<ValidationError> = [
{
property: 'REQUIRED_PROP',
constraints: { isDefined: 'REQUIRED_PROP should not be empty' },
value: undefined,
target: {},
children: []
}
]

const exception = new ValidationException('TestConfig', errors)

expect(exception.message).toContain('was: undefined')
})

it('clears the stack trace', () => {
const errors: Array<ValidationError> = [
{
property: 'PORT',
constraints: { isInt: 'PORT must be an integer' },
value: 'abc',
target: {},
children: []
}
]

const exception = new ValidationException('TestConfig', errors)

expect(exception.stack).toBeUndefined()
})
})
34 changes: 34 additions & 0 deletions lib/getters/get-config-value.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { HttpConfig, NodeConfig } from 'example/config'
import { getConfigValue } from './get-config-value'

describe('getConfigValue', () => {
it('returns a specific value from config using a getter', () => {
const port = getConfigValue(HttpConfig, config => config.HTTP_SERVICE_PORT)

expect(port).toEqual(3000)
})

it('returns a string value from config', () => {
const host = getConfigValue(HttpConfig, config => config.HTTP_SERVICE_HOST)

expect(host).toEqual('0.0.0.0')
})

it('returns a value from a dependency config', () => {
const env = getConfigValue(HttpConfig, config => config.node.environment)

expect(env).toBeDefined()
})

it('returns a method result via getter', () => {
const port = getConfigValue(HttpConfig, config => config.getHttpServicePort())

expect(port).toEqual(3000)
})

it('works with a config that has no dependencies', () => {
const env = getConfigValue(NodeConfig, config => config.environment)

expect(env).toBeDefined()
})
})
Loading
Loading