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
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
281 lines
8.8 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|