PSA/shared/workflow/runtime/__tests__/mappingValidator.test.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

281 lines
8.8 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
validateInputMapping,
validateInputMappingSchema,
collectSecretRefs,
collectSecretRefsFromConfig
} from '../validation/mappingValidator';
import type { InputMapping } from '../types';
describe('mappingValidator', () => {
describe('validateInputMapping', () => {
const baseOptions = {
stepPath: 'root.steps[0]',
stepId: 'step-1',
fieldName: 'inputMapping'
};
it('returns empty errors for undefined mapping', () => {
const result = validateInputMapping(undefined, baseOptions);
expect(result.errors).toHaveLength(0);
expect(result.warnings).toHaveLength(0);
expect(result.secretRefs.size).toBe(0);
});
it('returns empty errors for empty mapping', () => {
const result = validateInputMapping({}, baseOptions);
expect(result.errors).toHaveLength(0);
expect(result.warnings).toHaveLength(0);
});
it('validates expression syntax', () => {
const mapping: InputMapping = {
field1: { $expr: 'payload.name' }
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.errors).toHaveLength(0);
});
it('reports error for empty expression', () => {
const mapping: InputMapping = {
field1: { $expr: '' }
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].code).toBe('EMPTY_EXPRESSION');
});
it('reports error for invalid expression syntax', () => {
const mapping: InputMapping = {
field1: { $expr: 'payload..invalid' }
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].code).toBe('INVALID_EXPRESSION');
});
it('validates secret references', () => {
const mapping: InputMapping = {
field1: { $secret: 'API_KEY' }
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.errors).toHaveLength(0);
expect(result.secretRefs.has('API_KEY')).toBe(true);
});
it('reports error for invalid secret name format', () => {
const mapping: InputMapping = {
field1: { $secret: 'invalid secret name!' }
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.errors).toHaveLength(1);
expect(result.errors[0].code).toBe('INVALID_SECRET_NAME');
});
it('warns about unknown secrets when knownSecrets provided', () => {
const mapping: InputMapping = {
field1: { $secret: 'UNKNOWN_SECRET' }
};
const result = validateInputMapping(mapping, {
...baseOptions,
knownSecrets: new Set(['KNOWN_SECRET'])
});
expect(result.errors).toHaveLength(0);
expect(result.warnings).toHaveLength(1);
expect(result.warnings[0].code).toBe('UNKNOWN_SECRET');
});
it('does not warn about known secrets', () => {
const mapping: InputMapping = {
field1: { $secret: 'KNOWN_SECRET' }
};
const result = validateInputMapping(mapping, {
...baseOptions,
knownSecrets: new Set(['KNOWN_SECRET'])
});
expect(result.warnings).toHaveLength(0);
});
it('validates literal values', () => {
const mapping: InputMapping = {
stringField: 'hello',
numberField: 42,
boolField: true,
nullField: null
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.errors).toHaveLength(0);
});
it('validates nested arrays', () => {
const mapping: InputMapping = {
arrayField: [
{ $expr: 'payload.item1' },
{ $secret: 'SECRET1' },
'literal'
]
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.errors).toHaveLength(0);
expect(result.secretRefs.has('SECRET1')).toBe(true);
});
it('validates nested objects', () => {
const mapping: InputMapping = {
nestedField: {
inner: { $expr: 'payload.value' },
secret: { $secret: 'NESTED_SECRET' }
}
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.errors).toHaveLength(0);
expect(result.secretRefs.has('NESTED_SECRET')).toBe(true);
});
it('warns about unknown special keys', () => {
const mapping: InputMapping = {
field1: { $unknown: 'value' } as unknown as { $expr: string }
};
const result = validateInputMapping(mapping, baseOptions);
expect(result.warnings).toHaveLength(1);
expect(result.warnings[0].code).toBe('UNKNOWN_SPECIAL_KEY');
});
it('reports missing required fields', () => {
const mapping: InputMapping = {
optionalField: 'value'
};
const result = validateInputMapping(mapping, {
...baseOptions,
requiredFields: ['requiredField1', 'requiredField2']
});
expect(result.errors).toHaveLength(2);
expect(result.errors[0].code).toBe('MISSING_REQUIRED_MAPPING');
expect(result.errors[1].code).toBe('MISSING_REQUIRED_MAPPING');
});
it('does not report present required fields', () => {
const mapping: InputMapping = {
requiredField1: 'value1',
requiredField2: { $expr: 'payload.value' }
};
const result = validateInputMapping(mapping, {
...baseOptions,
requiredFields: ['requiredField1', 'requiredField2']
});
expect(result.errors).toHaveLength(0);
});
});
describe('validateInputMappingSchema', () => {
const baseOptions = {
stepPath: 'root.steps[0]',
stepId: 'step-1',
fieldName: 'inputMapping'
};
const schema = {
type: 'object',
required: ['foo'],
properties: {
foo: {
type: 'object',
required: ['bar'],
properties: {
bar: { type: 'string' }
}
}
}
};
it('reports missing top-level required field', () => {
const result = validateInputMappingSchema({}, schema, baseOptions);
expect(result).toHaveLength(1);
expect(result[0].code).toBe('MISSING_REQUIRED_MAPPING');
});
it('reports missing nested required field when parent is object literal', () => {
const result = validateInputMappingSchema({ foo: {} }, schema, baseOptions);
expect(result).toHaveLength(1);
expect(result[0].message).toContain('inputMapping.foo.bar');
});
it('does not report nested required when parent is mapped via expression', () => {
const result = validateInputMappingSchema({ foo: { $expr: 'payload.foo' } }, schema, baseOptions);
expect(result).toHaveLength(0);
});
it('passes when required fields are fully mapped', () => {
const result = validateInputMappingSchema({ foo: { bar: { $expr: 'payload.bar' } } }, schema, baseOptions);
expect(result).toHaveLength(0);
});
});
describe('collectSecretRefs', () => {
it('returns empty set for undefined mapping', () => {
const refs = collectSecretRefs(undefined);
expect(refs.size).toBe(0);
});
it('collects secret refs from simple mapping', () => {
const mapping: InputMapping = {
field1: { $secret: 'SECRET1' },
field2: { $secret: 'SECRET2' },
field3: { $expr: 'payload.value' }
};
const refs = collectSecretRefs(mapping);
expect(refs.size).toBe(2);
expect(refs.has('SECRET1')).toBe(true);
expect(refs.has('SECRET2')).toBe(true);
});
it('collects secret refs from nested structures', () => {
const mapping: InputMapping = {
nested: {
deep: { $secret: 'DEEP_SECRET' }
},
array: [
{ $secret: 'ARRAY_SECRET' }
]
};
const refs = collectSecretRefs(mapping);
expect(refs.size).toBe(2);
expect(refs.has('DEEP_SECRET')).toBe(true);
expect(refs.has('ARRAY_SECRET')).toBe(true);
});
});
describe('collectSecretRefsFromConfig', () => {
it('returns empty set for undefined config', () => {
const refs = collectSecretRefsFromConfig(undefined);
expect(refs.size).toBe(0);
});
it('collects secret refs from config object', () => {
const config = {
inputMapping: {
field1: { $secret: 'SECRET1' }
},
other: {
nested: { $secret: 'SECRET2' }
}
};
const refs = collectSecretRefsFromConfig(config);
expect(refs.size).toBe(2);
expect(refs.has('SECRET1')).toBe(true);
expect(refs.has('SECRET2')).toBe(true);
});
it('handles arrays in config', () => {
const config = {
items: [
{ $secret: 'SECRET1' },
{ value: { $secret: 'SECRET2' } }
]
};
const refs = collectSecretRefsFromConfig(config);
expect(refs.size).toBe(2);
});
});
});